1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::ops::Not as _;
3use std::{path::PathBuf, str::FromStr, time::Duration};
4
5use anyhow::{anyhow, ensure, Result};
6use boltz_client::{swaps::boltz::*, util::secrets::Preimage};
7use buy::{BuyBitcoinApi, BuyBitcoinService};
8use chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService};
9use chain_swap::ESTIMATED_BTC_CLAIM_TX_VSIZE;
10use futures_util::stream::select_all;
11use futures_util::{StreamExt, TryFutureExt};
12use lnurl::auth::SdkLnurlAuthSigner;
13use log::{debug, error, info, warn};
14use lwk_wollet::bitcoin::base64::Engine as _;
15use lwk_wollet::elements::AssetId;
16use lwk_wollet::elements_miniscript::elements::bitcoin::bip32::Xpub;
17use lwk_wollet::hashes::{sha256, Hash};
18use lwk_wollet::secp256k1::Message;
19use persist::model::PaymentTxDetails;
20use recover::recoverer::Recoverer;
21use sdk_common::bitcoin::hashes::hex::ToHex;
22use sdk_common::input_parser::InputType;
23use sdk_common::liquid::LiquidAddressData;
24use sdk_common::prelude::{FiatAPI, FiatCurrency, LnUrlPayError, LnUrlWithdrawError, Rate};
25use sdk_common::utils::Arc;
26use signer::SdkSigner;
27use swapper::boltz::proxy::BoltzProxyFetcher;
28use tokio::sync::{watch, RwLock};
29use tokio_stream::wrappers::BroadcastStream;
30use tokio_with_wasm::alias as tokio;
31use web_time::Instant;
32use x509_parser::parse_x509_certificate;
33
34use crate::chain_swap::ChainSwapHandler;
35use crate::ensure_sdk;
36use crate::error::SdkError;
37use crate::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription};
38use crate::model::PaymentState::*;
39use crate::model::Signer;
40use crate::payjoin::{side_swap::SideSwapPayjoinService, PayjoinService};
41use crate::receive_swap::ReceiveSwapHandler;
42use crate::send_swap::SendSwapHandler;
43use crate::swapper::SubscriptionHandler;
44use crate::swapper::{
45 boltz::BoltzSwapper, Swapper, SwapperStatusStream, SwapperSubscriptionHandler,
46};
47use crate::wallet::{LiquidOnchainWallet, OnchainWallet};
48use crate::{
49 error::{PaymentError, SdkResult},
50 event::EventManager,
51 model::*,
52 persist::Persister,
53 utils, *,
54};
55use sdk_common::lightning_with_bolt12::offers::invoice::Bolt12Invoice;
56
57use self::sync::client::BreezSyncerClient;
58use self::sync::SyncService;
59
60pub const DEFAULT_DATA_DIR: &str = ".data";
61pub const CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS: u32 = 4320;
63
64pub const DEFAULT_EXTERNAL_INPUT_PARSERS: &[(&str, &str, &str)] = &[(
67 "picknpay",
68 "(.*)(za.co.electrum.picknpay)(.*)",
69 "https://cryptoqr.net/.well-known/lnurlp/<input>",
70)];
71
72pub(crate) const NETWORK_PROPAGATION_GRACE_PERIOD: Duration = Duration::from_secs(30);
73
74pub struct LiquidSdkBuilder {
75 config: Config,
76 signer: Arc<Box<dyn Signer>>,
77 breez_server: Arc<BreezServer>,
78 bitcoin_chain_service: Option<Arc<dyn BitcoinChainService>>,
79 liquid_chain_service: Option<Arc<dyn LiquidChainService>>,
80 onchain_wallet: Option<Arc<dyn OnchainWallet>>,
81 payjoin_service: Option<Arc<dyn PayjoinService>>,
82 persister: Option<Arc<Persister>>,
83 recoverer: Option<Arc<Recoverer>>,
84 rest_client: Option<Arc<dyn RestClient>>,
85 status_stream: Option<Arc<dyn SwapperStatusStream>>,
86 swapper: Option<Arc<dyn Swapper>>,
87 sync_service: Option<Arc<SyncService>>,
88}
89
90#[allow(dead_code)]
91impl LiquidSdkBuilder {
92 pub fn new(
93 config: Config,
94 server_url: String,
95 signer: Arc<Box<dyn Signer>>,
96 ) -> Result<LiquidSdkBuilder> {
97 let breez_server = Arc::new(BreezServer::new(server_url, None)?);
98 Ok(LiquidSdkBuilder {
99 config,
100 signer,
101 breez_server,
102 bitcoin_chain_service: None,
103 liquid_chain_service: None,
104 onchain_wallet: None,
105 payjoin_service: None,
106 persister: None,
107 recoverer: None,
108 rest_client: None,
109 status_stream: None,
110 swapper: None,
111 sync_service: None,
112 })
113 }
114
115 pub fn bitcoin_chain_service(
116 &mut self,
117 bitcoin_chain_service: Arc<dyn BitcoinChainService>,
118 ) -> &mut Self {
119 self.bitcoin_chain_service = Some(bitcoin_chain_service.clone());
120 self
121 }
122
123 pub fn liquid_chain_service(
124 &mut self,
125 liquid_chain_service: Arc<dyn LiquidChainService>,
126 ) -> &mut Self {
127 self.liquid_chain_service = Some(liquid_chain_service.clone());
128 self
129 }
130
131 pub fn recoverer(&mut self, recoverer: Arc<Recoverer>) -> &mut Self {
132 self.recoverer = Some(recoverer.clone());
133 self
134 }
135
136 pub fn onchain_wallet(&mut self, onchain_wallet: Arc<dyn OnchainWallet>) -> &mut Self {
137 self.onchain_wallet = Some(onchain_wallet.clone());
138 self
139 }
140
141 pub fn payjoin_service(&mut self, payjoin_service: Arc<dyn PayjoinService>) -> &mut Self {
142 self.payjoin_service = Some(payjoin_service.clone());
143 self
144 }
145
146 pub fn persister(&mut self, persister: Arc<Persister>) -> &mut Self {
147 self.persister = Some(persister.clone());
148 self
149 }
150
151 pub fn rest_client(&mut self, rest_client: Arc<dyn RestClient>) -> &mut Self {
152 self.rest_client = Some(rest_client.clone());
153 self
154 }
155
156 pub fn status_stream(&mut self, status_stream: Arc<dyn SwapperStatusStream>) -> &mut Self {
157 self.status_stream = Some(status_stream.clone());
158 self
159 }
160
161 pub fn swapper(&mut self, swapper: Arc<dyn Swapper>) -> &mut Self {
162 self.swapper = Some(swapper.clone());
163 self
164 }
165
166 pub fn sync_service(&mut self, sync_service: Arc<SyncService>) -> &mut Self {
167 self.sync_service = Some(sync_service.clone());
168 self
169 }
170
171 fn get_working_dir(&self) -> Result<String> {
172 let fingerprint_hex: String =
173 Xpub::decode(self.signer.xpub()?.as_slice())?.identifier()[0..4].to_hex();
174 self.config
175 .get_wallet_dir(&self.config.working_dir, &fingerprint_hex)
176 }
177
178 pub async fn build(&self) -> Result<Arc<LiquidSdk>> {
179 if let Some(breez_api_key) = &self.config.breez_api_key {
180 LiquidSdk::validate_breez_api_key(breez_api_key)?
181 }
182
183 let fingerprint_hex: String =
184 Xpub::decode(self.signer.xpub()?.as_slice())?.identifier()[0..4].to_hex();
185 let cache_dir = self.config.get_wallet_dir(
186 self.config
187 .cache_dir
188 .as_ref()
189 .unwrap_or(&self.config.working_dir),
190 &fingerprint_hex,
191 )?;
192
193 let persister = match self.persister.clone() {
194 Some(persister) => persister,
195 None => {
196 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
197 return Err(anyhow!(
198 "Must provide a Wasm-compatible persister on Wasm builds"
199 ));
200 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
201 Arc::new(Persister::new_using_fs(
202 &self.get_working_dir()?,
203 self.config.network,
204 self.config.sync_enabled(),
205 self.config.asset_metadata.clone(),
206 )?)
207 }
208 };
209
210 let rest_client: Arc<dyn RestClient> = match self.rest_client.clone() {
211 Some(rest_client) => rest_client,
212 None => Arc::new(ReqwestRestClient::new()?),
213 };
214
215 let bitcoin_chain_service: Arc<dyn BitcoinChainService> =
216 match self.bitcoin_chain_service.clone() {
217 Some(bitcoin_chain_service) => bitcoin_chain_service,
218 None => self.config.bitcoin_chain_service(),
219 };
220
221 let liquid_chain_service: Arc<dyn LiquidChainService> =
222 match self.liquid_chain_service.clone() {
223 Some(liquid_chain_service) => liquid_chain_service,
224 None => self.config.liquid_chain_service()?,
225 };
226
227 let onchain_wallet: Arc<dyn OnchainWallet> = match self.onchain_wallet.clone() {
228 Some(onchain_wallet) => onchain_wallet,
229 None => Arc::new(
230 LiquidOnchainWallet::new(
231 self.config.clone(),
232 cache_dir,
233 persister.clone(),
234 self.signer.clone(),
235 )
236 .await?,
237 ),
238 };
239
240 let event_manager = Arc::new(EventManager::new());
241 let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(());
242
243 let (swapper, status_stream): (Arc<dyn Swapper>, Arc<dyn SwapperStatusStream>) =
244 match (self.swapper.clone(), self.status_stream.clone()) {
245 (Some(swapper), Some(status_stream)) => (swapper, status_stream),
246 (maybe_swapper, maybe_status_stream) => {
247 let proxy_url_fetcher = Arc::new(BoltzProxyFetcher::new(persister.clone()));
248 let boltz_swapper =
249 Arc::new(BoltzSwapper::new(self.config.clone(), proxy_url_fetcher)?);
250 (
251 maybe_swapper.unwrap_or(boltz_swapper.clone()),
252 maybe_status_stream.unwrap_or(boltz_swapper),
253 )
254 }
255 };
256
257 let recoverer = match self.recoverer.clone() {
258 Some(recoverer) => recoverer,
259 None => Arc::new(Recoverer::new(
260 self.signer.slip77_master_blinding_key()?,
261 swapper.clone(),
262 onchain_wallet.clone(),
263 liquid_chain_service.clone(),
264 bitcoin_chain_service.clone(),
265 persister.clone(),
266 )?),
267 };
268
269 let sync_service = match self.sync_service.clone() {
270 Some(sync_service) => Some(sync_service),
271 None => match self.config.sync_service_url.clone() {
272 Some(sync_service_url) => {
273 if BREEZ_SYNC_SERVICE_URL == sync_service_url
274 && self.config.breez_api_key.is_none()
275 {
276 anyhow::bail!(
277 "Cannot start the Breez real-time sync service without providing a valid API key. See https://sdk-doc-liquid.breez.technology/guide/getting_started.html#api-key",
278 );
279 }
280
281 let syncer_client =
282 Box::new(BreezSyncerClient::new(self.config.breez_api_key.clone()));
283 Some(Arc::new(SyncService::new(
284 sync_service_url,
285 persister.clone(),
286 recoverer.clone(),
287 self.signer.clone(),
288 syncer_client,
289 )))
290 }
291 None => None,
292 },
293 };
294
295 let send_swap_handler = SendSwapHandler::new(
296 self.config.clone(),
297 onchain_wallet.clone(),
298 persister.clone(),
299 swapper.clone(),
300 liquid_chain_service.clone(),
301 recoverer.clone(),
302 );
303
304 let receive_swap_handler = ReceiveSwapHandler::new(
305 self.config.clone(),
306 onchain_wallet.clone(),
307 persister.clone(),
308 swapper.clone(),
309 liquid_chain_service.clone(),
310 );
311
312 let chain_swap_handler = Arc::new(ChainSwapHandler::new(
313 self.config.clone(),
314 onchain_wallet.clone(),
315 persister.clone(),
316 swapper.clone(),
317 liquid_chain_service.clone(),
318 bitcoin_chain_service.clone(),
319 )?);
320
321 let payjoin_service = match self.payjoin_service.clone() {
322 Some(payjoin_service) => payjoin_service,
323 None => Arc::new(SideSwapPayjoinService::new(
324 self.config.clone(),
325 self.breez_server.clone(),
326 persister.clone(),
327 onchain_wallet.clone(),
328 rest_client.clone(),
329 )),
330 };
331
332 let buy_bitcoin_service = Arc::new(BuyBitcoinService::new(
333 self.config.clone(),
334 self.breez_server.clone(),
335 ));
336
337 let external_input_parsers = self.config.get_all_external_input_parsers();
338
339 let sdk = Arc::new(LiquidSdk {
340 config: self.config.clone(),
341 onchain_wallet,
342 signer: self.signer.clone(),
343 persister: persister.clone(),
344 rest_client,
345 event_manager,
346 status_stream: status_stream.clone(),
347 swapper,
348 recoverer,
349 bitcoin_chain_service,
350 liquid_chain_service,
351 fiat_api: self.breez_server.clone(),
352 is_started: RwLock::new(false),
353 shutdown_sender,
354 shutdown_receiver,
355 send_swap_handler,
356 receive_swap_handler,
357 sync_service,
358 chain_swap_handler,
359 payjoin_service,
360 buy_bitcoin_service,
361 external_input_parsers,
362 });
363 Ok(sdk)
364 }
365}
366
367pub struct LiquidSdk {
368 pub(crate) config: Config,
369 pub(crate) onchain_wallet: Arc<dyn OnchainWallet>,
370 pub(crate) signer: Arc<Box<dyn Signer>>,
371 pub(crate) persister: Arc<Persister>,
372 pub(crate) rest_client: Arc<dyn RestClient>,
373 pub(crate) event_manager: Arc<EventManager>,
374 pub(crate) status_stream: Arc<dyn SwapperStatusStream>,
375 pub(crate) swapper: Arc<dyn Swapper>,
376 pub(crate) recoverer: Arc<Recoverer>,
377 pub(crate) liquid_chain_service: Arc<dyn LiquidChainService>,
378 pub(crate) bitcoin_chain_service: Arc<dyn BitcoinChainService>,
379 pub(crate) fiat_api: Arc<dyn FiatAPI>,
380 pub(crate) is_started: RwLock<bool>,
381 pub(crate) shutdown_sender: watch::Sender<()>,
382 pub(crate) shutdown_receiver: watch::Receiver<()>,
383 pub(crate) send_swap_handler: SendSwapHandler,
384 pub(crate) sync_service: Option<Arc<SyncService>>,
385 pub(crate) receive_swap_handler: ReceiveSwapHandler,
386 pub(crate) chain_swap_handler: Arc<ChainSwapHandler>,
387 pub(crate) payjoin_service: Arc<dyn PayjoinService>,
388 pub(crate) buy_bitcoin_service: Arc<dyn BuyBitcoinApi>,
389 pub(crate) external_input_parsers: Vec<ExternalInputParser>,
390}
391
392impl LiquidSdk {
393 pub async fn connect(req: ConnectRequest) -> Result<Arc<LiquidSdk>> {
404 let signer = Self::default_signer(&req)?;
405
406 Self::connect_with_signer(
407 ConnectWithSignerRequest { config: req.config },
408 Box::new(signer),
409 )
410 .inspect_err(|e| error!("Failed to connect: {:?}", e))
411 .await
412 }
413
414 pub fn default_signer(req: &ConnectRequest) -> Result<SdkSigner> {
415 let is_mainnet = req.config.network == LiquidNetwork::Mainnet;
416 match (&req.mnemonic, &req.seed) {
417 (None, Some(seed)) => Ok(SdkSigner::new_with_seed(seed.clone(), is_mainnet)?),
418 (Some(mnemonic), None) => Ok(SdkSigner::new(
419 mnemonic,
420 req.passphrase.as_ref().unwrap_or(&"".to_string()).as_ref(),
421 is_mainnet,
422 )?),
423 _ => Err(anyhow!("Either `mnemonic` or `seed` must be set")),
424 }
425 }
426
427 pub async fn connect_with_signer(
428 req: ConnectWithSignerRequest,
429 signer: Box<dyn Signer>,
430 ) -> Result<Arc<LiquidSdk>> {
431 let start_ts = Instant::now();
432
433 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
434 std::fs::create_dir_all(&req.config.working_dir)?;
435
436 let sdk = LiquidSdkBuilder::new(
437 req.config,
438 PRODUCTION_BREEZSERVER_URL.into(),
439 Arc::new(signer),
440 )?
441 .build()
442 .await?;
443 sdk.start().await?;
444
445 let init_time = Instant::now().duration_since(start_ts);
446 utils::log_print_header(init_time);
447
448 Ok(sdk)
449 }
450
451 fn validate_breez_api_key(api_key: &str) -> Result<()> {
452 let api_key_decoded = lwk_wollet::bitcoin::base64::engine::general_purpose::STANDARD
453 .decode(api_key.as_bytes())
454 .map_err(|err| anyhow!("Could not base64 decode the Breez API key: {err:?}"))?;
455 let (_rem, cert) = parse_x509_certificate(&api_key_decoded)
456 .map_err(|err| anyhow!("Invaid certificate for Breez API key: {err:?}"))?;
457
458 let issuer = cert
459 .issuer()
460 .iter_common_name()
461 .next()
462 .and_then(|cn| cn.as_str().ok());
463 match issuer {
464 Some(common_name) => ensure_sdk!(
465 common_name.starts_with("Breez"),
466 anyhow!("Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted")
467 ),
468 _ => {
469 return Err(anyhow!("Could not parse Breez API key certificate: issuer is invalid or not found."))
470 }
471 }
472
473 Ok(())
474 }
475
476 pub async fn start(self: &Arc<LiquidSdk>) -> SdkResult<()> {
480 let mut is_started = self.is_started.write().await;
481 self.persister
482 .update_send_swaps_by_state(Created, TimedOut, Some(true))
483 .inspect_err(|e| error!("Failed to update send swaps by state: {:?}", e))?;
484
485 self.start_background_tasks()
486 .inspect_err(|e| error!("Failed to start background tasks: {:?}", e))
487 .await?;
488 *is_started = true;
489 Ok(())
490 }
491
492 async fn start_background_tasks(self: &Arc<LiquidSdk>) -> SdkResult<()> {
496 let subscription_handler = Box::new(SwapperSubscriptionHandler::new(
497 self.persister.clone(),
498 self.status_stream.clone(),
499 ));
500 self.status_stream
501 .clone()
502 .start(subscription_handler.clone(), self.shutdown_receiver.clone());
503 if let Some(sync_service) = self.sync_service.clone() {
504 sync_service.start(self.shutdown_receiver.clone());
505 }
506 self.start_track_new_blocks_task();
507 self.track_swap_updates();
508 self.track_realtime_sync_events(subscription_handler);
509
510 Ok(())
511 }
512
513 async fn ensure_is_started(&self) -> SdkResult<()> {
514 let is_started = self.is_started.read().await;
515 ensure_sdk!(*is_started, SdkError::NotStarted);
516 Ok(())
517 }
518
519 pub async fn disconnect(&self) -> SdkResult<()> {
521 self.ensure_is_started().await?;
522
523 let mut is_started = self.is_started.write().await;
524 self.shutdown_sender
525 .send(())
526 .map_err(|e| SdkError::generic(format!("Shutdown failed: {e}")))?;
527 *is_started = false;
528 Ok(())
529 }
530
531 fn track_realtime_sync_events(
532 self: &Arc<LiquidSdk>,
533 subscription_handler: Box<dyn SubscriptionHandler>,
534 ) {
535 let cloned = self.clone();
536 let Some(sync_service) = cloned.sync_service.clone() else {
537 return;
538 };
539 let mut shutdown_receiver = cloned.shutdown_receiver.clone();
540
541 tokio::spawn(async move {
542 let mut sync_events_receiver = sync_service.subscribe_events();
543 loop {
544 tokio::select! {
545 event = sync_events_receiver.recv() => {
546 if let Ok(e) = event {
547 match e {
548 sync::Event::SyncedCompleted{data} => {
549 info!(
550 "Received sync event: pulled {} records, pushed {} records",
551 data.pulled_records_count, data.pushed_records_count
552 );
553 let did_pull_new_records = data.pulled_records_count > 0;
554 if did_pull_new_records {
555 subscription_handler.subscribe_swaps().await;
556 }
557 cloned.notify_event_listeners(SdkEvent::DataSynced {did_pull_new_records}).await
558 }
559 }
560 }
561 }
562 _ = shutdown_receiver.changed() => {
563 info!("Received shutdown signal, exiting real-time sync loop");
564 return;
565 }
566 }
567 }
568 });
569 }
570
571 async fn track_new_blocks(
572 self: &Arc<LiquidSdk>,
573 current_liquid_block: &mut u32,
574 current_bitcoin_block: &mut u32,
575 ) {
576 info!("Track new blocks iteration started");
577 let t0 = Instant::now();
579 let liquid_tip_res = self.liquid_chain_service.tip().await;
580 let duration_ms = Instant::now().duration_since(t0).as_millis();
581 info!("Fetched liquid tip at ({duration_ms} ms)");
582
583 let is_new_liquid_block = match &liquid_tip_res {
584 Ok(height) => {
585 debug!("Got Liquid tip: {height}");
586 let is_new_liquid_block = *height > *current_liquid_block;
587 *current_liquid_block = *height;
588 is_new_liquid_block
589 }
590 Err(e) => {
591 error!("Failed to fetch Liquid tip {e}");
592 false
593 }
594 };
595 let t0 = Instant::now();
597 let bitcoin_tip_res = self.bitcoin_chain_service.tip().await;
598 let duration_ms = Instant::now().duration_since(t0).as_millis();
599 info!("Fetched bitcoin tip at ({duration_ms} ms)");
600 let is_new_bitcoin_block = match &bitcoin_tip_res {
601 Ok(height) => {
602 debug!("Got Bitcoin tip: {height}");
603 let is_new_bitcoin_block = *height > *current_bitcoin_block;
604 *current_bitcoin_block = *height;
605 is_new_bitcoin_block
606 }
607 Err(e) => {
608 error!("Failed to fetch Bitcoin tip {e}");
609 false
610 }
611 };
612
613 if let (Ok(liquid_tip), Ok(bitcoin_tip)) = (liquid_tip_res, bitcoin_tip_res) {
614 self.persister
615 .set_blockchain_info(&BlockchainInfo {
616 liquid_tip,
617 bitcoin_tip,
618 })
619 .unwrap_or_else(|err| warn!("Could not update local tips: {err:?}"));
620 };
621
622 let partial_sync = (is_new_liquid_block || is_new_bitcoin_block).not();
624 _ = self.sync(partial_sync).await;
625
626 if is_new_liquid_block {
628 self.chain_swap_handler
629 .on_liquid_block(*current_liquid_block)
630 .await;
631 self.receive_swap_handler
632 .on_liquid_block(*current_liquid_block)
633 .await;
634 self.send_swap_handler
635 .on_liquid_block(*current_liquid_block)
636 .await;
637 }
638 if is_new_bitcoin_block {
639 self.chain_swap_handler
640 .on_bitcoin_block(*current_bitcoin_block)
641 .await;
642 self.receive_swap_handler
643 .on_bitcoin_block(*current_liquid_block)
644 .await;
645 self.send_swap_handler
646 .on_bitcoin_block(*current_bitcoin_block)
647 .await;
648 }
649 }
650
651 fn start_track_new_blocks_task(self: &Arc<LiquidSdk>) {
652 let cloned = self.clone();
653 tokio::spawn(async move {
654 let mut current_liquid_block: u32 = 0;
655 let mut current_bitcoin_block: u32 = 0;
656 let mut shutdown_receiver = cloned.shutdown_receiver.clone();
657 cloned
658 .track_new_blocks(&mut current_liquid_block, &mut current_bitcoin_block)
659 .await;
660 loop {
661 tokio::select! {
662 _ = tokio::time::sleep(Duration::from_secs(10)) => {
663 cloned.track_new_blocks(&mut current_liquid_block, &mut current_bitcoin_block).await;
664 }
665
666 _ = shutdown_receiver.changed() => {
667 info!("Received shutdown signal, exiting track blocks loop");
668 return;
669 }
670 }
671 }
672 });
673 }
674
675 fn track_swap_updates(self: &Arc<LiquidSdk>) {
676 let cloned = self.clone();
677 tokio::spawn(async move {
678 let mut shutdown_receiver = cloned.shutdown_receiver.clone();
679 let mut updates_stream = cloned.status_stream.subscribe_swap_updates();
680 let swaps_streams = vec![
681 cloned.send_swap_handler.subscribe_payment_updates(),
682 cloned.receive_swap_handler.subscribe_payment_updates(),
683 cloned.chain_swap_handler.subscribe_payment_updates(),
684 ];
685 let mut combined_swap_streams =
686 select_all(swaps_streams.into_iter().map(BroadcastStream::new));
687 loop {
688 tokio::select! {
689 payment_id = combined_swap_streams.next() => {
690 if let Some(payment_id) = payment_id {
691 match payment_id {
692 Ok(payment_id) => {
693 if let Err(e) = cloned.emit_payment_updated(Some(payment_id)).await {
694 error!("Failed to emit payment update: {e:?}");
695 }
696 }
697 Err(e) => error!("Failed to receive swap state change: {e:?}")
698 }
699 }
700 }
701 update = updates_stream.recv() => match update {
702 Ok(update) => {
703 let id = &update.id;
704 match cloned.persister.fetch_swap_by_id(id) {
705 Ok(Swap::Send(_)) => match cloned.send_swap_handler.on_new_status(&update).await {
706 Ok(_) => info!("Successfully handled Send Swap {id} update"),
707 Err(e) => error!("Failed to handle Send Swap {id} update: {e}")
708 },
709 Ok(Swap::Receive(_)) => match cloned.receive_swap_handler.on_new_status(&update).await {
710 Ok(_) => info!("Successfully handled Receive Swap {id} update"),
711 Err(e) => error!("Failed to handle Receive Swap {id} update: {e}")
712 },
713 Ok(Swap::Chain(_)) => match cloned.chain_swap_handler.on_new_status(&update).await {
714 Ok(_) => info!("Successfully handled Chain Swap {id} update"),
715 Err(e) => error!("Failed to handle Chain Swap {id} update: {e}")
716 },
717 _ => {
718 error!("Could not find Swap {id}");
719 }
720 }
721 }
722 Err(e) => error!("Received stream error: {e:?}"),
723 },
724 _ = shutdown_receiver.changed() => {
725 info!("Received shutdown signal, exiting swap updates loop");
726 return;
727 }
728 }
729 }
730 });
731 }
732
733 async fn notify_event_listeners(&self, e: SdkEvent) {
734 self.event_manager.notify(e).await;
735 }
736
737 pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> SdkResult<String> {
744 Ok(self.event_manager.add(listener).await?)
745 }
746
747 pub async fn remove_event_listener(&self, id: String) -> SdkResult<()> {
753 self.event_manager.remove(id).await;
754 Ok(())
755 }
756
757 async fn emit_payment_updated(&self, payment_id: Option<String>) -> Result<()> {
758 if let Some(id) = payment_id {
759 match self.persister.get_payment(&id)? {
760 Some(payment) => {
761 self.update_wallet_info().await?;
762 match payment.status {
763 Complete => {
764 self.notify_event_listeners(SdkEvent::PaymentSucceeded {
765 details: payment,
766 })
767 .await
768 }
769 Pending => {
770 match &payment.details.get_swap_id() {
771 Some(swap_id) => match self.persister.fetch_swap_by_id(swap_id)? {
772 Swap::Chain(ChainSwap { claim_tx_id, .. }) => {
773 if claim_tx_id.is_some() {
774 self.notify_event_listeners(
776 SdkEvent::PaymentWaitingConfirmation {
777 details: payment,
778 },
779 )
780 .await
781 } else {
782 self.notify_event_listeners(SdkEvent::PaymentPending {
784 details: payment,
785 })
786 .await
787 }
788 }
789 Swap::Receive(ReceiveSwap {
790 claim_tx_id,
791 mrh_tx_id,
792 ..
793 }) => {
794 if claim_tx_id.is_some() || mrh_tx_id.is_some() {
795 self.notify_event_listeners(
797 SdkEvent::PaymentWaitingConfirmation {
798 details: payment,
799 },
800 )
801 .await
802 } else {
803 self.notify_event_listeners(SdkEvent::PaymentPending {
805 details: payment,
806 })
807 .await
808 }
809 }
810 Swap::Send(_) => {
811 self.notify_event_listeners(SdkEvent::PaymentPending {
813 details: payment,
814 })
815 .await
816 }
817 },
818 None => {
820 self.notify_event_listeners(
821 SdkEvent::PaymentWaitingConfirmation { details: payment },
822 )
823 .await
824 }
825 };
826 }
827 WaitingFeeAcceptance => {
828 let swap_id = &payment
829 .details
830 .get_swap_id()
831 .ok_or(anyhow!("Payment WaitingFeeAcceptance must have a swap"))?;
832
833 ensure!(
834 matches!(
835 self.persister.fetch_swap_by_id(swap_id)?,
836 Swap::Chain(ChainSwap { .. })
837 ),
838 "Swap in WaitingFeeAcceptance payment must be chain swap"
839 );
840
841 self.notify_event_listeners(SdkEvent::PaymentWaitingFeeAcceptance {
842 details: payment,
843 })
844 .await;
845 }
846 Refundable => {
847 self.notify_event_listeners(SdkEvent::PaymentRefundable {
848 details: payment,
849 })
850 .await
851 }
852 RefundPending => {
853 self.notify_event_listeners(SdkEvent::PaymentRefundPending {
855 details: payment,
856 })
857 .await
858 }
859 Failed => match payment.payment_type {
860 PaymentType::Receive => {
861 self.notify_event_listeners(SdkEvent::PaymentFailed {
862 details: payment,
863 })
864 .await
865 }
866 PaymentType::Send => {
867 self.notify_event_listeners(SdkEvent::PaymentRefunded {
869 details: payment,
870 })
871 .await
872 }
873 },
874 _ => (),
875 };
876 }
877 None => debug!("Payment not found: {id}"),
878 }
879 }
880 Ok(())
881 }
882
883 pub async fn get_info(&self) -> SdkResult<GetInfoResponse> {
885 self.ensure_is_started().await?;
886 let maybe_info = self.persister.get_info()?;
887 match maybe_info {
888 Some(info) => Ok(info),
889 None => {
890 self.update_wallet_info().await?;
891 self.persister.get_info()?.ok_or(SdkError::Generic {
892 err: "Info not found".into(),
893 })
894 }
895 }
896 }
897
898 pub fn sign_message(&self, req: &SignMessageRequest) -> SdkResult<SignMessageResponse> {
900 let signature = self.onchain_wallet.sign_message(&req.message)?;
901 Ok(SignMessageResponse { signature })
902 }
903
904 pub fn check_message(&self, req: &CheckMessageRequest) -> SdkResult<CheckMessageResponse> {
907 let is_valid =
908 self.onchain_wallet
909 .check_message(&req.message, &req.pubkey, &req.signature)?;
910 Ok(CheckMessageResponse { is_valid })
911 }
912
913 async fn validate_bitcoin_address(&self, input: &str) -> Result<String, PaymentError> {
914 match self.parse(input).await? {
915 InputType::BitcoinAddress {
916 address: bitcoin_address_data,
917 ..
918 } => match bitcoin_address_data.network == self.config.network.into() {
919 true => Ok(bitcoin_address_data.address),
920 false => Err(PaymentError::InvalidNetwork {
921 err: format!(
922 "Not a {} address",
923 Into::<Network>::into(self.config.network)
924 ),
925 }),
926 },
927 _ => Err(PaymentError::Generic {
928 err: "Invalid Bitcoin address".to_string(),
929 }),
930 }
931 }
932
933 fn validate_bolt11_invoice(&self, invoice: &str) -> Result<Bolt11Invoice, PaymentError> {
934 let invoice = invoice
935 .trim()
936 .parse::<Bolt11Invoice>()
937 .map_err(|err| PaymentError::invalid_invoice(&err.to_string()))?;
938
939 match (invoice.network().to_string().as_str(), self.config.network) {
940 ("bitcoin", LiquidNetwork::Mainnet) => {}
941 ("testnet", LiquidNetwork::Testnet) => {}
942 ("regtest", LiquidNetwork::Regtest) => {}
943 _ => {
944 return Err(PaymentError::InvalidNetwork {
945 err: "Invoice cannot be paid on the current network".to_string(),
946 })
947 }
948 }
949
950 let invoice_ts_web_time = web_time::SystemTime::UNIX_EPOCH
952 + invoice
953 .timestamp()
954 .duration_since(std::time::SystemTime::UNIX_EPOCH)
955 .map_err(|_| PaymentError::invalid_invoice("Invalid invoice timestamp"))?;
956 if let Ok(elapsed_web_time) =
957 web_time::SystemTime::now().duration_since(invoice_ts_web_time)
958 {
959 ensure_sdk!(
960 elapsed_web_time <= invoice.expiry_time(),
961 PaymentError::invalid_invoice("Invoice has expired")
962 )
963 }
964
965 Ok(invoice)
966 }
967
968 fn validate_bolt12_invoice(
969 &self,
970 offer: &LNOffer,
971 user_specified_receiver_amount_sat: u64,
972 invoice: &str,
973 ) -> Result<Bolt12Invoice, PaymentError> {
974 let invoice_parsed = utils::parse_bolt12_invoice(invoice)?;
975 let invoice_signing_pubkey = invoice_parsed.signing_pubkey().to_hex();
976
977 match &offer.signing_pubkey {
979 None => {
980 ensure_sdk!(
981 &offer
982 .paths
983 .iter()
984 .filter_map(|path| path.blinded_hops.last())
985 .any(|last_hop| &invoice_signing_pubkey == last_hop),
986 PaymentError::invalid_invoice(
987 "Invalid Bolt12 invoice signing key when using blinded path"
988 )
989 );
990 }
991 Some(offer_signing_pubkey) => {
992 ensure_sdk!(
993 offer_signing_pubkey == &invoice_signing_pubkey,
994 PaymentError::invalid_invoice("Invalid Bolt12 invoice signing key")
995 );
996 }
997 }
998
999 let receiver_amount_sat = invoice_parsed.amount_msats() / 1_000;
1000 ensure_sdk!(
1001 receiver_amount_sat == user_specified_receiver_amount_sat,
1002 PaymentError::invalid_invoice("Invalid Bolt12 invoice amount")
1003 );
1004
1005 Ok(invoice_parsed)
1006 }
1007
1008 async fn validate_submarine_pairs(
1011 &self,
1012 receiver_amount_sat: u64,
1013 ) -> Result<SubmarinePair, PaymentError> {
1014 let lbtc_pair = self
1015 .swapper
1016 .get_submarine_pairs()
1017 .await?
1018 .ok_or(PaymentError::PairsNotFound)?;
1019
1020 lbtc_pair.limits.within(receiver_amount_sat)?;
1021
1022 let fees_sat = lbtc_pair.fees.total(receiver_amount_sat);
1023
1024 ensure_sdk!(
1025 receiver_amount_sat > fees_sat,
1026 PaymentError::AmountOutOfRange
1027 );
1028
1029 Ok(lbtc_pair)
1030 }
1031
1032 async fn get_chain_pair(&self, direction: Direction) -> Result<ChainPair, PaymentError> {
1033 self.swapper
1034 .get_chain_pair(direction)
1035 .await?
1036 .ok_or(PaymentError::PairsNotFound)
1037 }
1038
1039 fn validate_user_lockup_amount_for_chain_pair(
1041 &self,
1042 pair: &ChainPair,
1043 user_lockup_amount_sat: u64,
1044 ) -> Result<(), PaymentError> {
1045 pair.limits.within(user_lockup_amount_sat)?;
1046
1047 let fees_sat = pair.fees.total(user_lockup_amount_sat);
1048 ensure_sdk!(
1049 user_lockup_amount_sat > fees_sat,
1050 PaymentError::AmountOutOfRange
1051 );
1052
1053 Ok(())
1054 }
1055
1056 async fn get_and_validate_chain_pair(
1057 &self,
1058 direction: Direction,
1059 user_lockup_amount_sat: Option<u64>,
1060 ) -> Result<ChainPair, PaymentError> {
1061 let pair = self.get_chain_pair(direction).await?;
1062 if let Some(user_lockup_amount_sat) = user_lockup_amount_sat {
1063 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
1064 }
1065 Ok(pair)
1066 }
1067
1068 async fn estimate_onchain_tx_fee(
1070 &self,
1071 amount_sat: u64,
1072 address: &str,
1073 asset_id: &str,
1074 ) -> Result<u64, PaymentError> {
1075 let fee_sat = self
1076 .onchain_wallet
1077 .build_tx(
1078 Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
1079 address,
1080 asset_id,
1081 amount_sat,
1082 )
1083 .await?
1084 .all_fees()
1085 .values()
1086 .sum::<u64>();
1087 info!("Estimated tx fee: {fee_sat} sat");
1088 Ok(fee_sat)
1089 }
1090
1091 fn get_temp_p2tr_addr(&self) -> &str {
1092 match self.config.network {
1095 LiquidNetwork::Mainnet => "lq1pqvzxvqhrf54dd4sny4cag7497pe38252qefk46t92frs7us8r80ja9ha8r5me09nn22m4tmdqp5p4wafq3s59cql3v9n45t5trwtxrmxfsyxjnstkctj",
1096 LiquidNetwork::Testnet => "tlq1pq0wqu32e2xacxeyps22x8gjre4qk3u6r70pj4r62hzczxeyz8x3yxucrpn79zy28plc4x37aaf33kwt6dz2nn6gtkya6h02mwpzy4eh69zzexq7cf5y5",
1097 LiquidNetwork::Regtest => "el1pqtjufhhy2se6lj2t7wufvpqqhnw66v57x2s0uu5dxs4fqlzlvh3hqe87vn83z3qreh8kxn49xe0h0fpe4kjkhl4gv99tdppupk0tdd485q8zegdag97r",
1098 }
1099 }
1100
1101 async fn estimate_lockup_tx_fee(
1103 &self,
1104 user_lockup_amount_sat: u64,
1105 ) -> Result<u64, PaymentError> {
1106 let temp_p2tr_addr = self.get_temp_p2tr_addr();
1107 self.estimate_onchain_tx_fee(
1108 user_lockup_amount_sat,
1109 temp_p2tr_addr,
1110 self.config.lbtc_asset_id().as_str(),
1111 )
1112 .await
1113 }
1114
1115 async fn estimate_drain_tx_fee(
1116 &self,
1117 enforce_amount_sat: Option<u64>,
1118 address: Option<&str>,
1119 ) -> Result<u64, PaymentError> {
1120 let receipent_address = address.unwrap_or(self.get_temp_p2tr_addr());
1121 let fee_sat = self
1122 .onchain_wallet
1123 .build_drain_tx(
1124 Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
1125 receipent_address,
1126 enforce_amount_sat,
1127 )
1128 .await?
1129 .all_fees()
1130 .values()
1131 .sum();
1132 info!("Estimated drain tx fee: {fee_sat} sat");
1133
1134 Ok(fee_sat)
1135 }
1136
1137 async fn estimate_onchain_tx_or_drain_tx_fee(
1138 &self,
1139 amount_sat: u64,
1140 address: &str,
1141 asset_id: &str,
1142 ) -> Result<u64, PaymentError> {
1143 match self
1144 .estimate_onchain_tx_fee(amount_sat, address, asset_id)
1145 .await
1146 {
1147 Ok(fees_sat) => Ok(fees_sat),
1148 Err(PaymentError::InsufficientFunds) if asset_id.eq(&self.config.lbtc_asset_id()) => {
1149 self.estimate_drain_tx_fee(Some(amount_sat), Some(address))
1150 .await
1151 .map_err(|_| PaymentError::InsufficientFunds)
1152 }
1153 Err(e) => Err(e),
1154 }
1155 }
1156
1157 async fn estimate_lockup_tx_or_drain_tx_fee(
1158 &self,
1159 amount_sat: u64,
1160 ) -> Result<u64, PaymentError> {
1161 let temp_p2tr_addr = self.get_temp_p2tr_addr();
1162 self.estimate_onchain_tx_or_drain_tx_fee(
1163 amount_sat,
1164 temp_p2tr_addr,
1165 &self.config.lbtc_asset_id(),
1166 )
1167 .await
1168 }
1169
1170 pub async fn prepare_send_payment(
1191 &self,
1192 req: &PrepareSendRequest,
1193 ) -> Result<PrepareSendResponse, PaymentError> {
1194 self.ensure_is_started().await?;
1195
1196 let get_info_res = self.get_info().await?;
1197 let fees_sat;
1198 let estimated_asset_fees;
1199 let receiver_amount_sat;
1200 let asset_id;
1201 let payment_destination;
1202
1203 match self.parse(&req.destination).await {
1204 Ok(InputType::LiquidAddress {
1205 address: mut liquid_address_data,
1206 }) => {
1207 let amount = match (
1208 liquid_address_data.amount,
1209 liquid_address_data.amount_sat,
1210 liquid_address_data.asset_id,
1211 req.amount.clone(),
1212 ) {
1213 (Some(amount), Some(amount_sat), Some(asset_id), None) => {
1214 if asset_id.eq(&self.config.lbtc_asset_id()) {
1215 PayAmount::Bitcoin {
1216 receiver_amount_sat: amount_sat,
1217 }
1218 } else {
1219 PayAmount::Asset {
1220 asset_id,
1221 receiver_amount: amount,
1222 estimate_asset_fees: None,
1223 }
1224 }
1225 }
1226 (_, Some(amount_sat), None, None) => PayAmount::Bitcoin {
1227 receiver_amount_sat: amount_sat,
1228 },
1229 (_, _, _, Some(amount)) => amount,
1230 _ => {
1231 return Err(PaymentError::AmountMissing {
1232 err: "Amount must be set when paying to a Liquid address".to_string(),
1233 });
1234 }
1235 };
1236
1237 ensure_sdk!(
1238 liquid_address_data.network == self.config.network.into(),
1239 PaymentError::InvalidNetwork {
1240 err: format!(
1241 "Cannot send payment from {} to {}",
1242 Into::<sdk_common::bitcoin::Network>::into(self.config.network),
1243 liquid_address_data.network
1244 )
1245 }
1246 );
1247
1248 (
1249 asset_id,
1250 receiver_amount_sat,
1251 fees_sat,
1252 estimated_asset_fees,
1253 ) = match amount {
1254 PayAmount::Drain => {
1255 ensure_sdk!(
1256 get_info_res.wallet_info.pending_receive_sat == 0
1257 && get_info_res.wallet_info.pending_send_sat == 0,
1258 PaymentError::Generic {
1259 err: "Cannot drain while there are pending payments".to_string(),
1260 }
1261 );
1262 let drain_fees_sat = self
1263 .estimate_drain_tx_fee(None, Some(&liquid_address_data.address))
1264 .await?;
1265 let drain_amount_sat =
1266 get_info_res.wallet_info.balance_sat - drain_fees_sat;
1267 info!("Drain amount: {drain_amount_sat} sat");
1268 (
1269 self.config.lbtc_asset_id(),
1270 drain_amount_sat,
1271 Some(drain_fees_sat),
1272 None,
1273 )
1274 }
1275 PayAmount::Bitcoin {
1276 receiver_amount_sat,
1277 } => {
1278 let asset_id = self.config.lbtc_asset_id();
1279 let fees_sat = self
1280 .estimate_onchain_tx_or_drain_tx_fee(
1281 receiver_amount_sat,
1282 &liquid_address_data.address,
1283 &asset_id,
1284 )
1285 .await?;
1286 (asset_id, receiver_amount_sat, Some(fees_sat), None)
1287 }
1288 PayAmount::Asset {
1289 asset_id,
1290 receiver_amount,
1291 estimate_asset_fees,
1292 } => {
1293 let estimate_asset_fees = estimate_asset_fees.unwrap_or(false);
1294 let asset_metadata = self.persister.get_asset_metadata(&asset_id)?.ok_or(
1295 PaymentError::AssetError {
1296 err: format!("Asset {asset_id} is not supported"),
1297 },
1298 )?;
1299 let receiver_amount_sat = asset_metadata.amount_to_sat(receiver_amount);
1300 let fees_sat_res = self
1301 .estimate_onchain_tx_or_drain_tx_fee(
1302 receiver_amount_sat,
1303 &liquid_address_data.address,
1304 &asset_id,
1305 )
1306 .await;
1307 let asset_fees = if estimate_asset_fees {
1308 self.payjoin_service
1309 .estimate_payjoin_tx_fee(&asset_id, receiver_amount_sat)
1310 .await
1311 .inspect_err(|e| debug!("Error estimating payjoin tx: {e}"))
1312 .ok()
1313 } else {
1314 None
1315 };
1316 let (fees_sat, asset_fees) = match (fees_sat_res, asset_fees) {
1317 (Ok(fees_sat), _) => (Some(fees_sat), asset_fees),
1318 (Err(e), Some(asset_fees)) => {
1319 debug!(
1320 "Error estimating onchain tx, but returning payjoin fees: {e}"
1321 );
1322 (None, Some(asset_fees))
1323 }
1324 (Err(e), None) => return Err(e),
1325 };
1326 (asset_id, receiver_amount_sat, fees_sat, asset_fees)
1327 }
1328 };
1329
1330 liquid_address_data.amount_sat = Some(receiver_amount_sat);
1331 liquid_address_data.asset_id = Some(asset_id.clone());
1332 payment_destination = SendDestination::LiquidAddress {
1333 address_data: liquid_address_data,
1334 bip353_address: None,
1335 };
1336 }
1337 Ok(InputType::Bolt11 { invoice }) => {
1338 self.ensure_send_is_not_self_transfer(&invoice.bolt11)?;
1339 self.validate_bolt11_invoice(&invoice.bolt11)?;
1340
1341 let invoice_amount_sat = invoice.amount_msat.ok_or(
1342 PaymentError::amount_missing("Expected invoice with an amount"),
1343 )? / 1000;
1344
1345 if let Some(PayAmount::Bitcoin {
1346 receiver_amount_sat: amount_sat,
1347 }) = req.amount
1348 {
1349 ensure_sdk!(
1350 invoice_amount_sat == amount_sat,
1351 PaymentError::Generic {
1352 err: "Receiver amount and invoice amount do not match".to_string()
1353 }
1354 );
1355 }
1356
1357 let lbtc_pair = self.validate_submarine_pairs(invoice_amount_sat).await?;
1358 let mrh_address = self
1359 .swapper
1360 .check_for_mrh(&invoice.bolt11)
1361 .await?
1362 .map(|(address, _)| address);
1363 asset_id = self.config.lbtc_asset_id();
1364 estimated_asset_fees = None;
1365 (receiver_amount_sat, fees_sat, payment_destination) =
1366 match (mrh_address.clone(), req.amount.clone()) {
1367 (Some(lbtc_address), Some(PayAmount::Drain)) => {
1368 let drain_fees_sat = self
1371 .estimate_drain_tx_fee(None, Some(&lbtc_address))
1372 .await?;
1373 let drain_amount_sat =
1374 get_info_res.wallet_info.balance_sat - drain_fees_sat;
1375 let payment_destination = SendDestination::LiquidAddress {
1376 address_data: LiquidAddressData {
1377 address: lbtc_address,
1378 asset_id: Some(asset_id.clone()),
1379 amount: None,
1380 amount_sat: Some(drain_amount_sat),
1381 network: self.config.network.into(),
1382 label: None,
1383 message: None,
1384 },
1385 bip353_address: None,
1386 };
1387 (drain_amount_sat, Some(drain_fees_sat), payment_destination)
1388 }
1389 (Some(lbtc_address), _) => {
1390 let fees_sat = self
1393 .estimate_onchain_tx_or_drain_tx_fee(
1394 invoice_amount_sat,
1395 &lbtc_address,
1396 &asset_id,
1397 )
1398 .await?;
1399 (
1400 invoice_amount_sat,
1401 Some(fees_sat),
1402 SendDestination::Bolt11 {
1403 invoice,
1404 bip353_address: None,
1405 },
1406 )
1407 }
1408 (None, _) => {
1409 let boltz_fees_total = lbtc_pair.fees.total(invoice_amount_sat);
1411 let user_lockup_amount_sat = invoice_amount_sat + boltz_fees_total;
1412 let lockup_fees_sat = self
1413 .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
1414 .await?;
1415 let fees_sat = boltz_fees_total + lockup_fees_sat;
1416 (
1417 invoice_amount_sat,
1418 Some(fees_sat),
1419 SendDestination::Bolt11 {
1420 invoice,
1421 bip353_address: None,
1422 },
1423 )
1424 }
1425 };
1426 }
1427 Ok(InputType::Bolt12Offer {
1428 offer,
1429 bip353_address,
1430 }) => {
1431 receiver_amount_sat = match req.amount {
1432 Some(PayAmount::Bitcoin {
1433 receiver_amount_sat: amount_sat,
1434 }) => Ok(amount_sat),
1435 _ => Err(PaymentError::amount_missing(
1436 "Expected PayAmount of type Receiver when processing a Bolt12 offer",
1437 )),
1438 }?;
1439 if let Some(Amount::Bitcoin { amount_msat }) = &offer.min_amount {
1440 ensure_sdk!(
1441 receiver_amount_sat >= amount_msat / 1_000,
1442 PaymentError::invalid_invoice(
1443 "Invalid receiver amount: below offer minimum"
1444 )
1445 );
1446 }
1447
1448 let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
1449
1450 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1451 let lockup_fees_sat = self
1452 .estimate_lockup_tx_or_drain_tx_fee(receiver_amount_sat + boltz_fees_total)
1453 .await?;
1454 asset_id = self.config.lbtc_asset_id();
1455 fees_sat = Some(boltz_fees_total + lockup_fees_sat);
1456 estimated_asset_fees = None;
1457
1458 payment_destination = SendDestination::Bolt12 {
1459 offer,
1460 receiver_amount_sat,
1461 bip353_address,
1462 };
1463 }
1464 _ => {
1465 return Err(PaymentError::generic("Destination is not valid"));
1466 }
1467 };
1468
1469 get_info_res.wallet_info.validate_sufficient_funds(
1470 self.config.network,
1471 receiver_amount_sat,
1472 fees_sat,
1473 &asset_id,
1474 )?;
1475
1476 Ok(PrepareSendResponse {
1477 destination: payment_destination,
1478 fees_sat,
1479 estimated_asset_fees,
1480 })
1481 }
1482
1483 fn ensure_send_is_not_self_transfer(&self, invoice: &str) -> Result<(), PaymentError> {
1484 match self.persister.fetch_receive_swap_by_invoice(invoice)? {
1485 None => Ok(()),
1486 Some(_) => Err(PaymentError::SelfTransferNotSupported),
1487 }
1488 }
1489
1490 pub async fn send_payment(
1506 &self,
1507 req: &SendPaymentRequest,
1508 ) -> Result<SendPaymentResponse, PaymentError> {
1509 self.ensure_is_started().await?;
1510
1511 let PrepareSendResponse {
1512 fees_sat,
1513 destination: payment_destination,
1514 ..
1515 } = &req.prepare_response;
1516
1517 match payment_destination {
1518 SendDestination::LiquidAddress {
1519 address_data: liquid_address_data,
1520 bip353_address,
1521 } => {
1522 let asset_pay_fees = req.use_asset_fees.unwrap_or_default();
1523 let Some(amount_sat) = liquid_address_data.amount_sat else {
1524 return Err(PaymentError::AmountMissing {
1525 err: "Amount must be set when paying to a Liquid address".to_string(),
1526 });
1527 };
1528 let Some(ref asset_id) = liquid_address_data.asset_id else {
1529 return Err(PaymentError::asset_error(
1530 "Asset must be set when paying to a Liquid address",
1531 ));
1532 };
1533
1534 ensure_sdk!(
1535 liquid_address_data.network == self.config.network.into(),
1536 PaymentError::InvalidNetwork {
1537 err: format!(
1538 "Cannot send payment from {} to {}",
1539 Into::<sdk_common::bitcoin::Network>::into(self.config.network),
1540 liquid_address_data.network
1541 )
1542 }
1543 );
1544
1545 self.get_info()
1546 .await?
1547 .wallet_info
1548 .validate_sufficient_funds(
1549 self.config.network,
1550 amount_sat,
1551 *fees_sat,
1552 asset_id,
1553 )?;
1554
1555 let mut response = if asset_pay_fees {
1556 self.pay_liquid_payjoin(liquid_address_data.clone(), amount_sat)
1557 .await?
1558 } else {
1559 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1560 self.pay_liquid(liquid_address_data.clone(), amount_sat, fees_sat, true)
1561 .await?
1562 };
1563
1564 self.insert_bip353_payment_details(bip353_address, &mut response)?;
1565 Ok(response)
1566 }
1567 SendDestination::Bolt11 {
1568 invoice,
1569 bip353_address,
1570 } => {
1571 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1572 let mut response = self.pay_bolt11_invoice(&invoice.bolt11, fees_sat).await?;
1573 self.insert_bip353_payment_details(bip353_address, &mut response)?;
1574 Ok(response)
1575 }
1576 SendDestination::Bolt12 {
1577 offer,
1578 receiver_amount_sat,
1579 bip353_address,
1580 } => {
1581 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1582 let bolt12_invoice = self
1583 .swapper
1584 .get_bolt12_invoice(&offer.offer, *receiver_amount_sat)
1585 .await?;
1586 let mut response = self
1587 .pay_bolt12_invoice(offer, *receiver_amount_sat, &bolt12_invoice, fees_sat)
1588 .await?;
1589 self.insert_bip353_payment_details(bip353_address, &mut response)?;
1590 Ok(response)
1591 }
1592 }
1593 }
1594
1595 fn insert_bip353_payment_details(
1596 &self,
1597 bip353_address: &Option<String>,
1598 response: &mut SendPaymentResponse,
1599 ) -> Result<()> {
1600 if bip353_address.is_some() {
1601 if let (Some(tx_id), Some(destination)) =
1602 (&response.payment.tx_id, &response.payment.destination)
1603 {
1604 self.persister
1605 .insert_or_update_payment_details(PaymentTxDetails {
1606 tx_id: tx_id.clone(),
1607 destination: destination.clone(),
1608 description: None,
1609 lnurl_info: None,
1610 bip353_address: bip353_address.clone(),
1611 asset_fees: None,
1612 })?;
1613 if let Some(payment) = self.persister.get_payment(tx_id)? {
1615 response.payment = payment;
1616 }
1617 }
1618 }
1619 Ok(())
1620 }
1621
1622 async fn pay_bolt11_invoice(
1623 &self,
1624 invoice: &str,
1625 fees_sat: u64,
1626 ) -> Result<SendPaymentResponse, PaymentError> {
1627 self.ensure_send_is_not_self_transfer(invoice)?;
1628 let bolt11_invoice = self.validate_bolt11_invoice(invoice)?;
1629
1630 let amount_sat = get_invoice_amount!(invoice);
1631 let payer_amount_sat = amount_sat + fees_sat;
1632 ensure_sdk!(
1633 payer_amount_sat <= self.get_info().await?.wallet_info.balance_sat,
1634 PaymentError::InsufficientFunds
1635 );
1636
1637 let description = match bolt11_invoice.description() {
1638 Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
1639 Bolt11InvoiceDescription::Hash(_) => None,
1640 };
1641
1642 match self.swapper.check_for_mrh(invoice).await? {
1643 Some((address, _)) => {
1645 info!("Found MRH for L-BTC address {address}, invoice amount_sat {amount_sat}");
1646 self.pay_liquid(
1647 LiquidAddressData {
1648 address,
1649 network: self.config.network.into(),
1650 asset_id: None,
1651 amount: None,
1652 amount_sat: None,
1653 label: None,
1654 message: None,
1655 },
1656 amount_sat,
1657 fees_sat,
1658 false,
1659 )
1660 .await
1661 }
1662
1663 None => {
1665 self.send_payment_via_swap(
1666 invoice,
1667 None,
1668 &bolt11_invoice.payment_hash().to_string(),
1669 description,
1670 amount_sat,
1671 fees_sat,
1672 )
1673 .await
1674 }
1675 }
1676 }
1677
1678 async fn pay_bolt12_invoice(
1679 &self,
1680 offer: &LNOffer,
1681 user_specified_receiver_amount_sat: u64,
1682 invoice_str: &str,
1683 fees_sat: u64,
1684 ) -> Result<SendPaymentResponse, PaymentError> {
1685 let invoice =
1686 self.validate_bolt12_invoice(offer, user_specified_receiver_amount_sat, invoice_str)?;
1687
1688 let receiver_amount_sat = invoice.amount_msats() / 1_000;
1689 let payer_amount_sat = receiver_amount_sat + fees_sat;
1690 ensure_sdk!(
1691 payer_amount_sat <= self.get_info().await?.wallet_info.balance_sat,
1692 PaymentError::InsufficientFunds
1693 );
1694
1695 self.send_payment_via_swap(
1696 invoice_str,
1697 Some(offer.offer.clone()),
1698 &invoice.payment_hash().to_string(),
1699 invoice.description().map(|desc| desc.to_string()),
1700 receiver_amount_sat,
1701 fees_sat,
1702 )
1703 .await
1704 }
1705
1706 async fn pay_liquid(
1708 &self,
1709 address_data: LiquidAddressData,
1710 receiver_amount_sat: u64,
1711 fees_sat: u64,
1712 skip_already_paid_check: bool,
1713 ) -> Result<SendPaymentResponse, PaymentError> {
1714 let destination = address_data
1715 .to_uri()
1716 .unwrap_or(address_data.address.clone());
1717 let asset_id = address_data.asset_id.unwrap_or(self.config.lbtc_asset_id());
1718 let payments = self.persister.get_payments(&ListPaymentsRequest {
1719 details: Some(ListPaymentDetails::Liquid {
1720 asset_id: Some(asset_id.clone()),
1721 destination: Some(destination.clone()),
1722 }),
1723 ..Default::default()
1724 })?;
1725 ensure_sdk!(
1726 skip_already_paid_check || payments.is_empty(),
1727 PaymentError::AlreadyPaid
1728 );
1729
1730 let tx = self
1731 .onchain_wallet
1732 .build_tx_or_drain_tx(
1733 Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
1734 &address_data.address,
1735 &asset_id,
1736 receiver_amount_sat,
1737 )
1738 .await?;
1739 let tx_id = tx.txid().to_string();
1740 let tx_fees_sat = tx.all_fees().values().sum::<u64>();
1741 ensure_sdk!(tx_fees_sat <= fees_sat, PaymentError::InvalidOrExpiredFees);
1742
1743 info!(
1744 "Built onchain Liquid tx with receiver_amount_sat = {receiver_amount_sat}, fees_sat = {fees_sat} and txid = {tx_id}"
1745 );
1746
1747 let tx_id = self.liquid_chain_service.broadcast(&tx).await?.to_string();
1748
1749 let tx_data = PaymentTxData {
1752 tx_id: tx_id.clone(),
1753 timestamp: Some(utils::now()),
1754 amount: receiver_amount_sat,
1755 fees_sat,
1756 payment_type: PaymentType::Send,
1757 is_confirmed: false,
1758 unblinding_data: None,
1759 asset_id: asset_id.clone(),
1760 };
1761
1762 let description = address_data.message;
1763
1764 self.persister.insert_or_update_payment(
1765 tx_data.clone(),
1766 Some(PaymentTxDetails {
1767 tx_id: tx_id.clone(),
1768 destination: destination.clone(),
1769 description: description.clone(),
1770 ..Default::default()
1771 }),
1772 false,
1773 )?;
1774 self.emit_payment_updated(Some(tx_id)).await?; let asset_info = self
1777 .persister
1778 .get_asset_metadata(&asset_id)?
1779 .map(|ref am| AssetInfo {
1780 name: am.name.clone(),
1781 ticker: am.ticker.clone(),
1782 amount: am.amount_from_sat(receiver_amount_sat),
1783 fees: None,
1784 });
1785 let payment_details = PaymentDetails::Liquid {
1786 asset_id,
1787 destination,
1788 description: description.unwrap_or("Liquid transfer".to_string()),
1789 asset_info,
1790 lnurl_info: None,
1791 bip353_address: None,
1792 };
1793
1794 Ok(SendPaymentResponse {
1795 payment: Payment::from_tx_data(tx_data, None, payment_details),
1796 })
1797 }
1798
1799 async fn pay_liquid_payjoin(
1801 &self,
1802 address_data: LiquidAddressData,
1803 receiver_amount_sat: u64,
1804 ) -> Result<SendPaymentResponse, PaymentError> {
1805 let destination = address_data
1806 .to_uri()
1807 .unwrap_or(address_data.address.clone());
1808 let Some(asset_id) = address_data.asset_id else {
1809 return Err(PaymentError::asset_error(
1810 "Asset must be set when paying to a Liquid address",
1811 ));
1812 };
1813
1814 let (tx, asset_fees) = self
1815 .payjoin_service
1816 .build_payjoin_tx(&address_data.address, &asset_id, receiver_amount_sat)
1817 .await
1818 .inspect_err(|e| error!("Error building payjoin tx: {e}"))?;
1819 let tx_id = tx.txid().to_string();
1820 let fees_sat = tx.all_fees().values().sum::<u64>();
1821
1822 info!(
1823 "Built payjoin Liquid tx with receiver_amount_sat = {receiver_amount_sat}, asset_fees = {asset_fees}, fees_sat = {fees_sat} and txid = {tx_id}"
1824 );
1825
1826 let tx_id = self.liquid_chain_service.broadcast(&tx).await?.to_string();
1827
1828 let tx_data = PaymentTxData {
1831 tx_id: tx_id.clone(),
1832 timestamp: Some(utils::now()),
1833 amount: receiver_amount_sat + asset_fees,
1834 fees_sat,
1835 payment_type: PaymentType::Send,
1836 is_confirmed: false,
1837 unblinding_data: None,
1838 asset_id: asset_id.clone(),
1839 };
1840
1841 let description = address_data.message;
1842
1843 self.persister.insert_or_update_payment(
1844 tx_data.clone(),
1845 Some(PaymentTxDetails {
1846 tx_id: tx_id.clone(),
1847 destination: destination.clone(),
1848 description: description.clone(),
1849 asset_fees: Some(asset_fees),
1850 ..Default::default()
1851 }),
1852 false,
1853 )?;
1854 self.emit_payment_updated(Some(tx_id)).await?; let asset_info = self
1857 .persister
1858 .get_asset_metadata(&asset_id)?
1859 .map(|ref am| AssetInfo {
1860 name: am.name.clone(),
1861 ticker: am.ticker.clone(),
1862 amount: am.amount_from_sat(receiver_amount_sat),
1863 fees: Some(am.amount_from_sat(asset_fees)),
1864 });
1865 let payment_details = PaymentDetails::Liquid {
1866 asset_id,
1867 destination,
1868 description: description.unwrap_or("Liquid transfer".to_string()),
1869 asset_info,
1870 lnurl_info: None,
1871 bip353_address: None,
1872 };
1873
1874 Ok(SendPaymentResponse {
1875 payment: Payment::from_tx_data(tx_data, None, payment_details),
1876 })
1877 }
1878
1879 async fn send_payment_via_swap(
1883 &self,
1884 invoice: &str,
1885 bolt12_offer: Option<String>,
1886 payment_hash: &str,
1887 description: Option<String>,
1888 receiver_amount_sat: u64,
1889 fees_sat: u64,
1890 ) -> Result<SendPaymentResponse, PaymentError> {
1891 let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
1892 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1893 let user_lockup_amount_sat = receiver_amount_sat + boltz_fees_total;
1894 let lockup_tx_fees_sat = self
1895 .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
1896 .await?;
1897 ensure_sdk!(
1898 fees_sat == boltz_fees_total + lockup_tx_fees_sat,
1899 PaymentError::InvalidOrExpiredFees
1900 );
1901
1902 let swap = match self.persister.fetch_send_swap_by_invoice(invoice)? {
1903 Some(swap) => match swap.state {
1904 Created => swap,
1905 TimedOut => {
1906 self.send_swap_handler.update_swap_info(
1907 &swap.id,
1908 PaymentState::Created,
1909 None,
1910 None,
1911 None,
1912 )?;
1913 swap
1914 }
1915 Pending => return Err(PaymentError::PaymentInProgress),
1916 Complete => return Err(PaymentError::AlreadyPaid),
1917 RefundPending | Refundable | Failed => {
1918 return Err(PaymentError::invalid_invoice(
1919 "Payment has already failed. Please try with another invoice",
1920 ))
1921 }
1922 WaitingFeeAcceptance => {
1923 return Err(PaymentError::Generic {
1924 err: "Send swap payment cannot be in state WaitingFeeAcceptance"
1925 .to_string(),
1926 })
1927 }
1928 },
1929 None => {
1930 let keypair = utils::generate_keypair();
1931 let refund_public_key = boltz_client::PublicKey {
1932 compressed: true,
1933 inner: keypair.public_key(),
1934 };
1935 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
1936 url,
1937 hash_swap_id: Some(true),
1938 status: Some(vec![
1939 SubSwapStates::InvoiceFailedToPay,
1940 SubSwapStates::SwapExpired,
1941 SubSwapStates::TransactionClaimPending,
1942 SubSwapStates::TransactionLockupFailed,
1943 ]),
1944 });
1945 let create_response = self
1946 .swapper
1947 .create_send_swap(CreateSubmarineRequest {
1948 from: "L-BTC".to_string(),
1949 to: "BTC".to_string(),
1950 invoice: invoice.to_string(),
1951 refund_public_key,
1952 pair_hash: Some(lbtc_pair.hash.clone()),
1953 referral_id: None,
1954 webhook,
1955 })
1956 .await?;
1957
1958 let swap_id = &create_response.id;
1959 let create_response_json =
1960 SendSwap::from_boltz_struct_to_json(&create_response, swap_id)?;
1961 let destination_pubkey =
1962 utils::get_invoice_destination_pubkey(invoice, bolt12_offer.is_some())?;
1963
1964 let payer_amount_sat = fees_sat + receiver_amount_sat;
1965 let swap = SendSwap {
1966 id: swap_id.to_string(),
1967 invoice: invoice.to_string(),
1968 bolt12_offer,
1969 payment_hash: Some(payment_hash.to_string()),
1970 destination_pubkey: Some(destination_pubkey),
1971 timeout_block_height: create_response.timeout_block_height,
1972 description,
1973 preimage: None,
1974 payer_amount_sat,
1975 receiver_amount_sat,
1976 pair_fees_json: serde_json::to_string(&lbtc_pair).map_err(|e| {
1977 PaymentError::generic(&format!("Failed to serialize SubmarinePair: {e:?}"))
1978 })?,
1979 create_response_json,
1980 lockup_tx_id: None,
1981 refund_address: None,
1982 refund_tx_id: None,
1983 created_at: utils::now(),
1984 state: PaymentState::Created,
1985 refund_private_key: keypair.display_secret().to_string(),
1986 metadata: Default::default(),
1987 };
1988 self.persister.insert_or_update_send_swap(&swap)?;
1989 swap
1990 }
1991 };
1992 self.status_stream.track_swap_id(&swap.id)?;
1993
1994 let create_response = swap.get_boltz_create_response()?;
1995 self.send_swap_handler
1996 .try_lockup(&swap, &create_response)
1997 .await?;
1998
1999 self.wait_for_payment_with_timeout(Swap::Send(swap), create_response.accept_zero_conf)
2000 .await
2001 .map(|payment| SendPaymentResponse { payment })
2002 }
2003
2004 pub async fn fetch_lightning_limits(
2006 &self,
2007 ) -> Result<LightningPaymentLimitsResponse, PaymentError> {
2008 self.ensure_is_started().await?;
2009
2010 let submarine_pair = self
2011 .swapper
2012 .get_submarine_pairs()
2013 .await?
2014 .ok_or(PaymentError::PairsNotFound)?;
2015 let send_limits = submarine_pair.limits;
2016
2017 let reverse_pair = self
2018 .swapper
2019 .get_reverse_swap_pairs()
2020 .await?
2021 .ok_or(PaymentError::PairsNotFound)?;
2022 let receive_limits = reverse_pair.limits;
2023
2024 Ok(LightningPaymentLimitsResponse {
2025 send: Limits {
2026 min_sat: send_limits.minimal_batched.unwrap_or(send_limits.minimal),
2027 max_sat: send_limits.maximal,
2028 max_zero_conf_sat: send_limits.maximal_zero_conf,
2029 },
2030 receive: Limits {
2031 min_sat: receive_limits.minimal,
2032 max_sat: receive_limits.maximal,
2033 max_zero_conf_sat: self.config.zero_conf_max_amount_sat(),
2034 },
2035 })
2036 }
2037
2038 pub async fn fetch_onchain_limits(&self) -> Result<OnchainPaymentLimitsResponse, PaymentError> {
2040 self.ensure_is_started().await?;
2041
2042 let (pair_outgoing, pair_incoming) = self.swapper.get_chain_pairs().await?;
2043 let send_limits = pair_outgoing
2044 .ok_or(PaymentError::PairsNotFound)
2045 .map(|pair| pair.limits)?;
2046 let receive_limits = pair_incoming
2047 .ok_or(PaymentError::PairsNotFound)
2048 .map(|pair| pair.limits)?;
2049
2050 Ok(OnchainPaymentLimitsResponse {
2051 send: Limits {
2052 min_sat: send_limits.minimal,
2053 max_sat: send_limits.maximal,
2054 max_zero_conf_sat: send_limits.maximal_zero_conf,
2055 },
2056 receive: Limits {
2057 min_sat: receive_limits.minimal,
2058 max_sat: receive_limits.maximal,
2059 max_zero_conf_sat: receive_limits.maximal_zero_conf,
2060 },
2061 })
2062 }
2063
2064 pub async fn prepare_pay_onchain(
2073 &self,
2074 req: &PreparePayOnchainRequest,
2075 ) -> Result<PreparePayOnchainResponse, PaymentError> {
2076 self.ensure_is_started().await?;
2077
2078 let get_info_res = self.get_info().await?;
2079 let pair = self.get_chain_pair(Direction::Outgoing).await?;
2080 let claim_fees_sat = match req.fee_rate_sat_per_vbyte {
2081 Some(sat_per_vbyte) => ESTIMATED_BTC_CLAIM_TX_VSIZE * sat_per_vbyte as u64,
2082 None => pair.clone().fees.claim_estimate(),
2083 };
2084 let server_fees_sat = pair.fees.server();
2085
2086 info!("Preparing for onchain payment of kind: {:?}", req.amount);
2087 let (payer_amount_sat, receiver_amount_sat, total_fees_sat) = match req.amount {
2088 PayAmount::Bitcoin {
2089 receiver_amount_sat: amount_sat,
2090 } => {
2091 let receiver_amount_sat = amount_sat;
2092
2093 let user_lockup_amount_sat_without_service_fee =
2094 receiver_amount_sat + claim_fees_sat + server_fees_sat;
2095
2096 let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64
2099 * 100.0
2100 / (100.0 - pair.fees.percentage))
2101 .ceil() as u64;
2102 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2103
2104 let lockup_fees_sat = self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?;
2105
2106 let boltz_fees_sat =
2107 user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2108 let total_fees_sat =
2109 boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2110 let payer_amount_sat = receiver_amount_sat + total_fees_sat;
2111
2112 (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2113 }
2114 PayAmount::Drain => {
2115 ensure_sdk!(
2116 get_info_res.wallet_info.pending_receive_sat == 0
2117 && get_info_res.wallet_info.pending_send_sat == 0,
2118 PaymentError::Generic {
2119 err: "Cannot drain while there are pending payments".to_string(),
2120 }
2121 );
2122 let payer_amount_sat = get_info_res.wallet_info.balance_sat;
2123 let lockup_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
2124
2125 let user_lockup_amount_sat = payer_amount_sat - lockup_fees_sat;
2126 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2127
2128 let boltz_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
2129 let total_fees_sat =
2130 boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2131 let receiver_amount_sat = payer_amount_sat - total_fees_sat;
2132
2133 (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2134 }
2135 PayAmount::Asset { .. } => {
2136 return Err(PaymentError::asset_error(
2137 "Cannot send an asset to a Bitcoin address",
2138 ))
2139 }
2140 };
2141
2142 let res = PreparePayOnchainResponse {
2143 receiver_amount_sat,
2144 claim_fees_sat,
2145 total_fees_sat,
2146 };
2147
2148 ensure_sdk!(
2149 payer_amount_sat <= get_info_res.wallet_info.balance_sat,
2150 PaymentError::InsufficientFunds
2151 );
2152
2153 info!("Prepared onchain payment: {res:?}");
2154 Ok(res)
2155 }
2156
2157 pub async fn pay_onchain(
2174 &self,
2175 req: &PayOnchainRequest,
2176 ) -> Result<SendPaymentResponse, PaymentError> {
2177 self.ensure_is_started().await?;
2178 info!("Paying onchain, request = {req:?}");
2179
2180 let claim_address = self.validate_bitcoin_address(&req.address).await?;
2181 let balance_sat = self.get_info().await?.wallet_info.balance_sat;
2182 let receiver_amount_sat = req.prepare_response.receiver_amount_sat;
2183 let pair = self.get_chain_pair(Direction::Outgoing).await?;
2184 let claim_fees_sat = req.prepare_response.claim_fees_sat;
2185 let server_fees_sat = pair.fees.server();
2186 let server_lockup_amount_sat = receiver_amount_sat + claim_fees_sat;
2187
2188 let user_lockup_amount_sat_without_service_fee =
2189 receiver_amount_sat + claim_fees_sat + server_fees_sat;
2190
2191 let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64 * 100.0
2194 / (100.0 - pair.fees.percentage))
2195 .ceil() as u64;
2196 let boltz_fee_sat = user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2197 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2198
2199 let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2200
2201 let lockup_fees_sat = match payer_amount_sat == balance_sat {
2202 true => self.estimate_drain_tx_fee(None, None).await?,
2203 false => self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?,
2204 };
2205
2206 ensure_sdk!(
2207 req.prepare_response.total_fees_sat
2208 == boltz_fee_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat,
2209 PaymentError::InvalidOrExpiredFees
2210 );
2211
2212 ensure_sdk!(
2213 payer_amount_sat <= balance_sat,
2214 PaymentError::InsufficientFunds
2215 );
2216
2217 let preimage = Preimage::new();
2218 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2219
2220 let claim_keypair = utils::generate_keypair();
2221 let claim_public_key = boltz_client::PublicKey {
2222 compressed: true,
2223 inner: claim_keypair.public_key(),
2224 };
2225 let refund_keypair = utils::generate_keypair();
2226 let refund_public_key = boltz_client::PublicKey {
2227 compressed: true,
2228 inner: refund_keypair.public_key(),
2229 };
2230 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2231 url,
2232 hash_swap_id: Some(true),
2233 status: Some(vec![
2234 ChainSwapStates::TransactionFailed,
2235 ChainSwapStates::TransactionLockupFailed,
2236 ChainSwapStates::TransactionServerConfirmed,
2237 ]),
2238 });
2239 let create_response = self
2240 .swapper
2241 .create_chain_swap(CreateChainRequest {
2242 from: "L-BTC".to_string(),
2243 to: "BTC".to_string(),
2244 preimage_hash: preimage.sha256,
2245 claim_public_key: Some(claim_public_key),
2246 refund_public_key: Some(refund_public_key),
2247 user_lock_amount: None,
2248 server_lock_amount: Some(server_lockup_amount_sat),
2249 pair_hash: Some(pair.hash.clone()),
2250 referral_id: None,
2251 webhook,
2252 })
2253 .await?;
2254
2255 let create_response_json =
2256 ChainSwap::from_boltz_struct_to_json(&create_response, &create_response.id)?;
2257 let swap_id = create_response.id;
2258
2259 let accept_zero_conf = server_lockup_amount_sat <= pair.limits.maximal_zero_conf;
2260 let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2261
2262 let swap = ChainSwap {
2263 id: swap_id.clone(),
2264 direction: Direction::Outgoing,
2265 claim_address: Some(claim_address),
2266 lockup_address: create_response.lockup_details.lockup_address,
2267 refund_address: None,
2268 timeout_block_height: create_response.lockup_details.timeout_block_height,
2269 preimage: preimage_str,
2270 description: Some("Bitcoin transfer".to_string()),
2271 payer_amount_sat,
2272 actual_payer_amount_sat: None,
2273 receiver_amount_sat,
2274 accepted_receiver_amount_sat: None,
2275 claim_fees_sat,
2276 pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
2277 PaymentError::generic(&format!("Failed to serialize outgoing ChainPair: {e:?}"))
2278 })?,
2279 accept_zero_conf,
2280 create_response_json,
2281 claim_private_key: claim_keypair.display_secret().to_string(),
2282 refund_private_key: refund_keypair.display_secret().to_string(),
2283 server_lockup_tx_id: None,
2284 user_lockup_tx_id: None,
2285 claim_tx_id: None,
2286 refund_tx_id: None,
2287 created_at: utils::now(),
2288 state: PaymentState::Created,
2289 auto_accepted_fees: false,
2290 metadata: Default::default(),
2291 };
2292 self.persister.insert_or_update_chain_swap(&swap)?;
2293 self.status_stream.track_swap_id(&swap_id)?;
2294
2295 self.wait_for_payment_with_timeout(Swap::Chain(swap), accept_zero_conf)
2296 .await
2297 .map(|payment| SendPaymentResponse { payment })
2298 }
2299
2300 async fn wait_for_payment_with_timeout(
2301 &self,
2302 swap: Swap,
2303 accept_zero_conf: bool,
2304 ) -> Result<Payment, PaymentError> {
2305 let timeout_fut = tokio::time::sleep(Duration::from_secs(self.config.payment_timeout_sec));
2306 tokio::pin!(timeout_fut);
2307
2308 let expected_swap_id = swap.id();
2309 let mut events_stream = self.event_manager.subscribe();
2310 let mut maybe_payment: Option<Payment> = None;
2311
2312 loop {
2313 tokio::select! {
2314 _ = &mut timeout_fut => match maybe_payment {
2315 Some(payment) => return Ok(payment),
2316 None => {
2317 debug!("Timeout occurred without payment, set swap to timed out");
2318 let update_res = match swap {
2319 Swap::Send(_) => self.send_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None),
2320 Swap::Chain(_) => self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
2321 swap_id: expected_swap_id.clone(),
2322 to_state: TimedOut,
2323 ..Default::default()
2324 }),
2325 _ => Ok(())
2326 };
2327 return match update_res {
2328 Ok(_) => Err(PaymentError::PaymentTimeout),
2329 Err(_) => {
2330 self.persister.get_payment(&expected_swap_id).ok().flatten().ok_or(PaymentError::generic("Payment not found"))
2333 }
2334 }
2335 },
2336 },
2337 event = events_stream.recv() => match event {
2338 Ok(SdkEvent::PaymentPending { details: payment }) => {
2339 let maybe_payment_swap_id = payment.details.get_swap_id();
2340 if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2341 match accept_zero_conf {
2342 true => {
2343 debug!("Received Send Payment pending event with zero-conf accepted");
2344 return Ok(payment)
2345 }
2346 false => {
2347 debug!("Received Send Payment pending event, waiting for confirmation");
2348 maybe_payment = Some(payment);
2349 }
2350 }
2351 };
2352 },
2353 Ok(SdkEvent::PaymentSucceeded { details: payment }) => {
2354 let maybe_payment_swap_id = payment.details.get_swap_id();
2355 if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2356 debug!("Received Send Payment succeed event");
2357 return Ok(payment);
2358 }
2359 },
2360 Ok(event) => debug!("Unhandled event waiting for payment: {event:?}"),
2361 Err(e) => debug!("Received error waiting for payment: {e:?}"),
2362 }
2363 }
2364 }
2365 }
2366
2367 pub async fn prepare_receive_payment(
2377 &self,
2378 req: &PrepareReceiveRequest,
2379 ) -> Result<PrepareReceiveResponse, PaymentError> {
2380 self.ensure_is_started().await?;
2381
2382 let mut min_payer_amount_sat = None;
2383 let mut max_payer_amount_sat = None;
2384 let mut swapper_feerate = None;
2385 let fees_sat;
2386 match req.payment_method {
2387 PaymentMethod::Lightning => {
2388 let payer_amount_sat = match req.amount {
2389 Some(ReceiveAmount::Asset { .. }) => {
2390 return Err(PaymentError::asset_error(
2391 "Cannot receive an asset when the payment method is Lightning",
2392 ));
2393 }
2394 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
2395 None => {
2396 return Err(PaymentError::generic(
2397 "Bitcoin payer amount must be set when the payment method is Lightning",
2398 ));
2399 }
2400 };
2401 let reverse_pair = self
2402 .swapper
2403 .get_reverse_swap_pairs()
2404 .await?
2405 .ok_or(PaymentError::PairsNotFound)?;
2406
2407 fees_sat = reverse_pair.fees.total(payer_amount_sat);
2408
2409 ensure_sdk!(payer_amount_sat > fees_sat, PaymentError::AmountOutOfRange);
2410
2411 reverse_pair
2412 .limits
2413 .within(payer_amount_sat)
2414 .map_err(|_| PaymentError::AmountOutOfRange)?;
2415
2416 min_payer_amount_sat = Some(reverse_pair.limits.minimal);
2417 max_payer_amount_sat = Some(reverse_pair.limits.maximal);
2418 swapper_feerate = Some(reverse_pair.fees.percentage);
2419
2420 debug!(
2421 "Preparing Lightning Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat"
2422 );
2423 }
2424 PaymentMethod::BitcoinAddress => {
2425 let payer_amount_sat = match req.amount {
2426 Some(ReceiveAmount::Asset { .. }) => {
2427 return Err(PaymentError::asset_error(
2428 "Cannot receive an asset when the payment method is Bitcoin",
2429 ));
2430 }
2431 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
2432 None => None,
2433 };
2434 let pair = self
2435 .get_and_validate_chain_pair(Direction::Incoming, payer_amount_sat)
2436 .await?;
2437 let claim_fees_sat = pair.fees.claim_estimate();
2438 let server_fees_sat = pair.fees.server();
2439 let service_fees_sat = payer_amount_sat
2440 .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
2441 .unwrap_or_default();
2442
2443 min_payer_amount_sat = Some(pair.limits.minimal);
2444 max_payer_amount_sat = Some(pair.limits.maximal);
2445 swapper_feerate = Some(pair.fees.percentage);
2446
2447 fees_sat = service_fees_sat + claim_fees_sat + server_fees_sat;
2448 debug!("Preparing Chain Receive Swap with: payer_amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}");
2449 }
2450 PaymentMethod::LiquidAddress => {
2451 let (asset_id, payer_amount, payer_amount_sat) = match req.amount.clone() {
2452 Some(ReceiveAmount::Asset {
2453 payer_amount,
2454 asset_id,
2455 }) => (asset_id, payer_amount, None),
2456 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
2457 (self.config.lbtc_asset_id(), None, Some(payer_amount_sat))
2458 }
2459 None => (self.config.lbtc_asset_id(), None, None),
2460 };
2461 fees_sat = 0;
2462 debug!("Preparing Liquid Receive with: asset_id {asset_id}, amount {payer_amount:?}, amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}");
2463 }
2464 };
2465
2466 Ok(PrepareReceiveResponse {
2467 amount: req.amount.clone(),
2468 fees_sat,
2469 payment_method: req.payment_method.clone(),
2470 min_payer_amount_sat,
2471 max_payer_amount_sat,
2472 swapper_feerate,
2473 })
2474 }
2475
2476 pub async fn receive_payment(
2491 &self,
2492 req: &ReceivePaymentRequest,
2493 ) -> Result<ReceivePaymentResponse, PaymentError> {
2494 self.ensure_is_started().await?;
2495
2496 let PrepareReceiveResponse {
2497 payment_method,
2498 amount,
2499 fees_sat,
2500 ..
2501 } = &req.prepare_response;
2502
2503 match payment_method {
2504 PaymentMethod::Lightning => {
2505 let amount_sat = match amount.clone() {
2506 Some(ReceiveAmount::Asset { .. }) => {
2507 return Err(PaymentError::asset_error(
2508 "Cannot receive an asset when the payment method is Lightning",
2509 ));
2510 }
2511 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
2512 None => {
2513 return Err(PaymentError::generic(
2514 "Bitcoin payer amount must be set when the payment method is Lightning",
2515 ));
2516 }
2517 };
2518 let (description, description_hash) = match (
2519 req.description.clone(),
2520 req.use_description_hash.unwrap_or_default(),
2521 ) {
2522 (Some(description), true) => (
2523 None,
2524 Some(sha256::Hash::hash(description.as_bytes()).to_hex()),
2525 ),
2526 (_, false) => (req.description.clone(), None),
2527 _ => {
2528 return Err(PaymentError::InvalidDescription {
2529 err: "Missing payment description to hash".to_string(),
2530 })
2531 }
2532 };
2533 self.create_receive_swap(amount_sat, *fees_sat, description, description_hash)
2534 .await
2535 }
2536 PaymentMethod::BitcoinAddress => {
2537 let amount_sat = match amount.clone() {
2538 Some(ReceiveAmount::Asset { .. }) => {
2539 return Err(PaymentError::asset_error(
2540 "Cannot receive an asset when the payment method is Bitcoin",
2541 ));
2542 }
2543 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
2544 None => None,
2545 };
2546 self.receive_onchain(amount_sat, *fees_sat).await
2547 }
2548 PaymentMethod::LiquidAddress => {
2549 let lbtc_asset_id = self.config.lbtc_asset_id();
2550 let (asset_id, amount, amount_sat) = match amount.clone() {
2551 Some(ReceiveAmount::Asset {
2552 asset_id,
2553 payer_amount,
2554 }) => (asset_id, payer_amount, None),
2555 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
2556 (lbtc_asset_id.clone(), None, Some(payer_amount_sat))
2557 }
2558 None => (lbtc_asset_id.clone(), None, None),
2559 };
2560
2561 let address = self.onchain_wallet.next_unused_address().await?.to_string();
2562 let receive_destination =
2563 if asset_id.ne(&lbtc_asset_id) || amount.is_some() || amount_sat.is_some() {
2564 LiquidAddressData {
2565 address: address.to_string(),
2566 network: self.config.network.into(),
2567 amount,
2568 amount_sat,
2569 asset_id: Some(asset_id),
2570 label: None,
2571 message: req.description.clone(),
2572 }
2573 .to_uri()
2574 .map_err(|e| PaymentError::Generic {
2575 err: format!("Could not build BIP21 URI: {e:?}"),
2576 })?
2577 } else {
2578 address
2579 };
2580
2581 Ok(ReceivePaymentResponse {
2582 destination: receive_destination,
2583 })
2584 }
2585 }
2586 }
2587
2588 async fn create_receive_swap(
2589 &self,
2590 payer_amount_sat: u64,
2591 fees_sat: u64,
2592 description: Option<String>,
2593 description_hash: Option<String>,
2594 ) -> Result<ReceivePaymentResponse, PaymentError> {
2595 let reverse_pair = self
2596 .swapper
2597 .get_reverse_swap_pairs()
2598 .await?
2599 .ok_or(PaymentError::PairsNotFound)?;
2600 let new_fees_sat = reverse_pair.fees.total(payer_amount_sat);
2601 ensure_sdk!(fees_sat == new_fees_sat, PaymentError::InvalidOrExpiredFees);
2602
2603 debug!("Creating Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
2604
2605 let keypair = utils::generate_keypair();
2606
2607 let preimage = Preimage::new();
2608 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2609 let preimage_hash = preimage.sha256.to_string();
2610
2611 let mrh_addr = self.onchain_wallet.next_unused_address().await?;
2613
2614 let mrh_addr_str = mrh_addr.to_string();
2616 let mrh_addr_hash = sha256::Hash::hash(mrh_addr_str.as_bytes());
2617 let mrh_addr_hash_sig =
2618 keypair.sign_schnorr(Message::from_digest_slice(mrh_addr_hash.as_byte_array())?);
2619
2620 let receiver_amount_sat = payer_amount_sat - fees_sat;
2621 let webhook_claim_status =
2622 match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
2623 true => RevSwapStates::TransactionConfirmed,
2624 false => RevSwapStates::TransactionMempool,
2625 };
2626 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2627 url,
2628 hash_swap_id: Some(true),
2629 status: Some(vec![webhook_claim_status]),
2630 });
2631
2632 let v2_req = CreateReverseRequest {
2633 invoice_amount: payer_amount_sat,
2634 from: "BTC".to_string(),
2635 to: "L-BTC".to_string(),
2636 preimage_hash: preimage.sha256,
2637 claim_public_key: keypair.public_key().into(),
2638 description,
2639 description_hash,
2640 address: Some(mrh_addr_str.clone()),
2641 address_signature: Some(mrh_addr_hash_sig.to_hex()),
2642 referral_id: None,
2643 webhook,
2644 };
2645 let create_response = self.swapper.create_receive_swap(v2_req).await?;
2646
2647 self.persister.insert_or_update_reserved_address(
2649 &mrh_addr_str,
2650 create_response.timeout_block_height,
2651 )?;
2652
2653 let (bip21_lbtc_address, _bip21_amount_btc) = self
2655 .swapper
2656 .check_for_mrh(&create_response.invoice)
2657 .await?
2658 .ok_or(PaymentError::receive_error("Invoice has no MRH"))?;
2659 ensure_sdk!(
2660 bip21_lbtc_address == mrh_addr_str,
2661 PaymentError::receive_error("Invoice has incorrect address in MRH")
2662 );
2663
2664 let swap_id = create_response.id.clone();
2665 let invoice = Bolt11Invoice::from_str(&create_response.invoice)
2666 .map_err(|err| PaymentError::invalid_invoice(&err.to_string()))?;
2667 let payer_amount_sat =
2668 invoice
2669 .amount_milli_satoshis()
2670 .ok_or(PaymentError::invalid_invoice(
2671 "Invoice does not contain an amount",
2672 ))?
2673 / 1000;
2674 let destination_pubkey = invoice_pubkey(&invoice);
2675
2676 ensure_sdk!(
2679 invoice.payment_hash().to_string() == preimage_hash,
2680 PaymentError::invalid_invoice("Invalid preimage returned by swapper")
2681 );
2682
2683 let create_response_json = ReceiveSwap::from_boltz_struct_to_json(
2684 &create_response,
2685 &swap_id,
2686 &invoice.to_string(),
2687 )?;
2688 let invoice_description = match invoice.description() {
2689 Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
2690 Bolt11InvoiceDescription::Hash(_) => None,
2691 };
2692
2693 self.persister
2694 .insert_or_update_receive_swap(&ReceiveSwap {
2695 id: swap_id.clone(),
2696 preimage: preimage_str,
2697 create_response_json,
2698 claim_private_key: keypair.display_secret().to_string(),
2699 invoice: invoice.to_string(),
2700 payment_hash: Some(preimage_hash),
2701 destination_pubkey: Some(destination_pubkey),
2702 timeout_block_height: create_response.timeout_block_height,
2703 description: invoice_description,
2704 payer_amount_sat,
2705 receiver_amount_sat,
2706 pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
2707 PaymentError::generic(&format!("Failed to serialize ReversePair: {e:?}"))
2708 })?,
2709 claim_fees_sat: reverse_pair.fees.claim_estimate(),
2710 lockup_tx_id: None,
2711 claim_address: None,
2712 claim_tx_id: None,
2713 mrh_address: mrh_addr_str,
2714 mrh_tx_id: None,
2715 created_at: utils::now(),
2716 state: PaymentState::Created,
2717 metadata: Default::default(),
2718 })
2719 .map_err(|_| PaymentError::PersistError)?;
2720 self.status_stream.track_swap_id(&swap_id)?;
2721
2722 Ok(ReceivePaymentResponse {
2723 destination: invoice.to_string(),
2724 })
2725 }
2726
2727 async fn create_receive_chain_swap(
2728 &self,
2729 user_lockup_amount_sat: Option<u64>,
2730 fees_sat: u64,
2731 ) -> Result<ChainSwap, PaymentError> {
2732 let pair = self
2733 .get_and_validate_chain_pair(Direction::Incoming, user_lockup_amount_sat)
2734 .await?;
2735 let claim_fees_sat = pair.fees.claim_estimate();
2736 let server_fees_sat = pair.fees.server();
2737 let service_fees_sat = user_lockup_amount_sat
2739 .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
2740 .unwrap_or_default();
2741
2742 ensure_sdk!(
2743 fees_sat == service_fees_sat + claim_fees_sat + server_fees_sat,
2744 PaymentError::InvalidOrExpiredFees
2745 );
2746
2747 let preimage = Preimage::new();
2748 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2749
2750 let claim_keypair = utils::generate_keypair();
2751 let claim_public_key = boltz_client::PublicKey {
2752 compressed: true,
2753 inner: claim_keypair.public_key(),
2754 };
2755 let refund_keypair = utils::generate_keypair();
2756 let refund_public_key = boltz_client::PublicKey {
2757 compressed: true,
2758 inner: refund_keypair.public_key(),
2759 };
2760 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2761 url,
2762 hash_swap_id: Some(true),
2763 status: Some(vec![
2764 ChainSwapStates::TransactionFailed,
2765 ChainSwapStates::TransactionLockupFailed,
2766 ChainSwapStates::TransactionServerConfirmed,
2767 ]),
2768 });
2769 let create_response = self
2770 .swapper
2771 .create_chain_swap(CreateChainRequest {
2772 from: "BTC".to_string(),
2773 to: "L-BTC".to_string(),
2774 preimage_hash: preimage.sha256,
2775 claim_public_key: Some(claim_public_key),
2776 refund_public_key: Some(refund_public_key),
2777 user_lock_amount: user_lockup_amount_sat,
2778 server_lock_amount: None,
2779 pair_hash: Some(pair.hash.clone()),
2780 referral_id: None,
2781 webhook,
2782 })
2783 .await?;
2784
2785 let swap_id = create_response.id.clone();
2786 let create_response_json =
2787 ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?;
2788
2789 let accept_zero_conf = user_lockup_amount_sat
2790 .map(|user_lockup_amount_sat| user_lockup_amount_sat <= pair.limits.maximal_zero_conf)
2791 .unwrap_or(false);
2792 let receiver_amount_sat = user_lockup_amount_sat
2793 .map(|user_lockup_amount_sat| user_lockup_amount_sat - fees_sat)
2794 .unwrap_or(0);
2795
2796 let swap = ChainSwap {
2797 id: swap_id.clone(),
2798 direction: Direction::Incoming,
2799 claim_address: None,
2800 lockup_address: create_response.lockup_details.lockup_address,
2801 refund_address: None,
2802 timeout_block_height: create_response.lockup_details.timeout_block_height,
2803 preimage: preimage_str,
2804 description: Some("Bitcoin transfer".to_string()),
2805 payer_amount_sat: user_lockup_amount_sat.unwrap_or(0),
2806 actual_payer_amount_sat: None,
2807 receiver_amount_sat,
2808 accepted_receiver_amount_sat: None,
2809 claim_fees_sat,
2810 pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
2811 PaymentError::generic(&format!("Failed to serialize incoming ChainPair: {e:?}"))
2812 })?,
2813 accept_zero_conf,
2814 create_response_json,
2815 claim_private_key: claim_keypair.display_secret().to_string(),
2816 refund_private_key: refund_keypair.display_secret().to_string(),
2817 server_lockup_tx_id: None,
2818 user_lockup_tx_id: None,
2819 claim_tx_id: None,
2820 refund_tx_id: None,
2821 created_at: utils::now(),
2822 state: PaymentState::Created,
2823 auto_accepted_fees: false,
2824 metadata: Default::default(),
2825 };
2826 self.persister.insert_or_update_chain_swap(&swap)?;
2827 self.status_stream.track_swap_id(&swap.id)?;
2828 Ok(swap)
2829 }
2830
2831 async fn receive_onchain(
2836 &self,
2837 user_lockup_amount_sat: Option<u64>,
2838 fees_sat: u64,
2839 ) -> Result<ReceivePaymentResponse, PaymentError> {
2840 self.ensure_is_started().await?;
2841
2842 let swap = self
2843 .create_receive_chain_swap(user_lockup_amount_sat, fees_sat)
2844 .await?;
2845 let create_response = swap.get_boltz_create_response()?;
2846 let address = create_response.lockup_details.lockup_address;
2847
2848 let amount = create_response.lockup_details.amount as f64 / 100_000_000.0;
2849 let bip21 = create_response.lockup_details.bip21.unwrap_or(format!(
2850 "bitcoin:{address}?amount={amount}&label=Send%20to%20L-BTC%20address"
2851 ));
2852
2853 Ok(ReceivePaymentResponse { destination: bip21 })
2854 }
2855
2856 pub async fn list_refundables(&self) -> SdkResult<Vec<RefundableSwap>> {
2859 let chain_swaps = self.persister.list_refundable_chain_swaps()?;
2860
2861 let mut lockup_script_pubkeys = vec![];
2862 for swap in &chain_swaps {
2863 let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
2864 lockup_script_pubkeys.push(script_pubkey);
2865 }
2866 let lockup_scripts: Vec<&boltz_client::bitcoin::Script> = lockup_script_pubkeys
2867 .iter()
2868 .map(|s| s.as_script())
2869 .collect();
2870 let scripts_utxos = self
2871 .bitcoin_chain_service
2872 .get_scripts_utxos(&lockup_scripts)
2873 .await?;
2874
2875 let mut refundables = vec![];
2876 for (chain_swap, script_utxos) in chain_swaps.into_iter().zip(scripts_utxos) {
2877 let swap_id = &chain_swap.id;
2878 let amount_sat = script_utxos
2879 .iter()
2880 .filter_map(|utxo| utxo.as_bitcoin().cloned())
2881 .map(|(_, txo)| txo.value.to_sat())
2882 .sum();
2883 info!("Incoming Chain Swap {swap_id} is refundable with {amount_sat} sats");
2884
2885 let refundable: RefundableSwap = chain_swap.to_refundable(amount_sat);
2886 refundables.push(refundable);
2887 }
2888
2889 Ok(refundables)
2890 }
2891
2892 pub async fn prepare_refund(
2901 &self,
2902 req: &PrepareRefundRequest,
2903 ) -> SdkResult<PrepareRefundResponse> {
2904 let refund_address = self
2905 .validate_bitcoin_address(&req.refund_address)
2906 .await
2907 .map_err(|e| SdkError::Generic {
2908 err: format!("Failed to validate refund address: {e}"),
2909 })?;
2910
2911 let (tx_vsize, tx_fee_sat, refund_tx_id) = self
2912 .chain_swap_handler
2913 .prepare_refund(
2914 &req.swap_address,
2915 &refund_address,
2916 req.fee_rate_sat_per_vbyte,
2917 )
2918 .await?;
2919 Ok(PrepareRefundResponse {
2920 tx_vsize,
2921 tx_fee_sat,
2922 last_refund_tx_id: refund_tx_id,
2923 })
2924 }
2925
2926 pub async fn refund(&self, req: &RefundRequest) -> Result<RefundResponse, PaymentError> {
2935 let refund_address = self
2936 .validate_bitcoin_address(&req.refund_address)
2937 .await
2938 .map_err(|e| SdkError::Generic {
2939 err: format!("Failed to validate refund address: {e}"),
2940 })?;
2941
2942 let refund_tx_id = self
2943 .chain_swap_handler
2944 .refund_incoming_swap(
2945 &req.swap_address,
2946 &refund_address,
2947 req.fee_rate_sat_per_vbyte,
2948 true,
2949 )
2950 .or_else(|e| {
2951 warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
2952 self.chain_swap_handler.refund_incoming_swap(
2953 &req.swap_address,
2954 &refund_address,
2955 req.fee_rate_sat_per_vbyte,
2956 false,
2957 )
2958 })
2959 .await?;
2960
2961 Ok(RefundResponse { refund_tx_id })
2962 }
2963
2964 pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> {
2972 let t0 = Instant::now();
2973 let mut rescannable_swaps: Vec<Swap> = self
2974 .persister
2975 .list_chain_swaps()?
2976 .into_iter()
2977 .map(Into::into)
2978 .collect();
2979 self.recoverer
2980 .recover_from_onchain(&mut rescannable_swaps)
2981 .await?;
2982 let scanned_len = rescannable_swaps.len();
2983 for swap in rescannable_swaps {
2984 let swap_id = &swap.id();
2985 if let Swap::Chain(chain_swap) = swap {
2986 if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
2987 error!("Error persisting rescanned Chain Swap {swap_id}: {e}");
2988 }
2989 }
2990 }
2991 info!(
2992 "Rescanned {} chain swaps in {} seconds",
2993 scanned_len,
2994 t0.elapsed().as_millis()
2995 );
2996 Ok(())
2997 }
2998
2999 fn validate_buy_bitcoin(&self, amount_sat: u64) -> Result<(), PaymentError> {
3000 ensure_sdk!(
3001 self.config.network == LiquidNetwork::Mainnet,
3002 PaymentError::invalid_network("Can only buy bitcoin on Mainnet")
3003 );
3004 ensure_sdk!(
3006 amount_sat % 1_000 == 0,
3007 PaymentError::generic("Can only buy sat amounts that are multiples of 1000")
3008 );
3009 Ok(())
3010 }
3011
3012 pub async fn prepare_buy_bitcoin(
3020 &self,
3021 req: &PrepareBuyBitcoinRequest,
3022 ) -> Result<PrepareBuyBitcoinResponse, PaymentError> {
3023 self.validate_buy_bitcoin(req.amount_sat)?;
3024
3025 let res = self
3026 .prepare_receive_payment(&PrepareReceiveRequest {
3027 payment_method: PaymentMethod::BitcoinAddress,
3028 amount: Some(ReceiveAmount::Bitcoin {
3029 payer_amount_sat: req.amount_sat,
3030 }),
3031 })
3032 .await?;
3033
3034 let Some(ReceiveAmount::Bitcoin {
3035 payer_amount_sat: amount_sat,
3036 }) = res.amount
3037 else {
3038 return Err(PaymentError::Generic {
3039 err: format!(
3040 "Error preparing receive payment, got amount: {:?}",
3041 res.amount
3042 ),
3043 });
3044 };
3045
3046 Ok(PrepareBuyBitcoinResponse {
3047 provider: req.provider,
3048 amount_sat,
3049 fees_sat: res.fees_sat,
3050 })
3051 }
3052
3053 pub async fn buy_bitcoin(&self, req: &BuyBitcoinRequest) -> Result<String, PaymentError> {
3061 self.validate_buy_bitcoin(req.prepare_response.amount_sat)?;
3062
3063 let swap = self
3064 .create_receive_chain_swap(
3065 Some(req.prepare_response.amount_sat),
3066 req.prepare_response.fees_sat,
3067 )
3068 .await?;
3069
3070 Ok(self
3071 .buy_bitcoin_service
3072 .buy_bitcoin(
3073 req.prepare_response.provider,
3074 &swap,
3075 req.redirect_url.clone(),
3076 )
3077 .await?)
3078 }
3079
3080 pub(crate) async fn get_monitored_swaps_list(&self, partial_sync: bool) -> Result<Vec<Swap>> {
3081 let receive_swaps = self
3082 .persister
3083 .list_recoverable_receive_swaps()?
3084 .into_iter()
3085 .map(Into::into)
3086 .collect();
3087 match partial_sync {
3088 false => {
3089 let bitcoin_height = self.bitcoin_chain_service.tip().await?;
3090 let liquid_height = self.liquid_chain_service.tip().await?;
3091 let final_swap_states = [PaymentState::Complete, PaymentState::Failed];
3092
3093 let send_swaps = self
3094 .persister
3095 .list_recoverable_send_swaps()?
3096 .into_iter()
3097 .map(Into::into)
3098 .collect();
3099 let chain_swaps: Vec<Swap> = self
3100 .persister
3101 .list_chain_swaps()?
3102 .into_iter()
3103 .filter(|swap| match swap.direction {
3104 Direction::Incoming => {
3105 bitcoin_height
3106 <= swap.timeout_block_height
3107 + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS
3108 }
3109 Direction::Outgoing => {
3110 !final_swap_states.contains(&swap.state)
3111 && liquid_height <= swap.timeout_block_height
3112 }
3113 })
3114 .map(Into::into)
3115 .collect();
3116 Ok([receive_swaps, send_swaps, chain_swaps].concat())
3117 }
3118 true => Ok(receive_swaps),
3119 }
3120 }
3121
3122 async fn sync_payments_with_chain_data(&self, partial_sync: bool) -> Result<()> {
3125 let mut recoverable_swaps = self.get_monitored_swaps_list(partial_sync).await?;
3126 let mut wallet_tx_map = self
3127 .recoverer
3128 .recover_from_onchain(&mut recoverable_swaps)
3129 .await?;
3130
3131 let all_wallet_tx_ids: HashSet<String> =
3132 wallet_tx_map.keys().map(|txid| txid.to_string()).collect();
3133
3134 for swap in recoverable_swaps {
3135 let swap_id = &swap.id();
3136
3137 match swap {
3139 Swap::Receive(receive_swap) => {
3140 let history_updates = vec![&receive_swap.claim_tx_id, &receive_swap.mrh_tx_id];
3141 for tx_id in history_updates
3142 .into_iter()
3143 .flatten()
3144 .collect::<Vec<&String>>()
3145 {
3146 if let Some(tx) =
3147 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
3148 {
3149 self.persister
3150 .insert_or_update_payment_with_wallet_tx(&tx)?;
3151 }
3152 }
3153 if let Err(e) = self.receive_swap_handler.update_swap(receive_swap) {
3154 error!("Error persisting recovered receive swap {swap_id}: {e}");
3155 }
3156 }
3157 Swap::Send(send_swap) => {
3158 let history_updates = vec![&send_swap.lockup_tx_id, &send_swap.refund_tx_id];
3159 for tx_id in history_updates
3160 .into_iter()
3161 .flatten()
3162 .collect::<Vec<&String>>()
3163 {
3164 if let Some(tx) =
3165 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
3166 {
3167 self.persister
3168 .insert_or_update_payment_with_wallet_tx(&tx)?;
3169 }
3170 }
3171 if let Err(e) = self.send_swap_handler.update_swap(send_swap) {
3172 error!("Error persisting recovered send swap {swap_id}: {e}");
3173 }
3174 }
3175 Swap::Chain(chain_swap) => {
3176 let history_updates = match chain_swap.direction {
3177 Direction::Incoming => vec![&chain_swap.claim_tx_id],
3178 Direction::Outgoing => {
3179 vec![&chain_swap.user_lockup_tx_id, &chain_swap.refund_tx_id]
3180 }
3181 };
3182 for tx_id in history_updates
3183 .into_iter()
3184 .flatten()
3185 .collect::<Vec<&String>>()
3186 {
3187 if let Some(tx) =
3188 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
3189 {
3190 self.persister
3191 .insert_or_update_payment_with_wallet_tx(&tx)?;
3192 }
3193 }
3194 if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
3195 error!("Error persisting recovered Chain Swap {swap_id}: {e}");
3196 }
3197 }
3198 };
3199 }
3200
3201 let non_swap_wallet_tx_map = wallet_tx_map;
3202
3203 let payments = self
3204 .persister
3205 .get_payments_by_tx_id(&ListPaymentsRequest::default())?;
3206
3207 let unconfirmed_payment_txs_data = self.persister.list_unconfirmed_payment_txs_data()?;
3209 let unconfirmed_txs_by_id: HashMap<String, PaymentTxData> = unconfirmed_payment_txs_data
3210 .into_iter()
3211 .map(|tx| (tx.tx_id.clone(), tx))
3212 .collect::<HashMap<String, PaymentTxData>>();
3213
3214 for tx in non_swap_wallet_tx_map.values() {
3215 let tx_id = tx.txid.to_string();
3216 let maybe_payment = payments.get(&tx_id);
3217 let mut updated = false;
3218 match maybe_payment {
3219 None
3221 | Some(Payment {
3222 details: PaymentDetails::Liquid { .. },
3223 ..
3224 }) => {
3225 let updated_needed = maybe_payment
3226 .is_none_or(|payment| payment.status == Pending && tx.height.is_some());
3227 if updated_needed {
3228 self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
3231 self.emit_payment_updated(Some(tx_id.clone())).await?;
3232 updated = true
3233 }
3234 }
3235
3236 _ => {}
3237 }
3238 if !updated && unconfirmed_txs_by_id.contains_key(&tx_id) && tx.height.is_some() {
3239 self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
3241 }
3242 }
3243
3244 let unknown_unconfirmed_txs: Vec<_> = unconfirmed_txs_by_id
3245 .iter()
3246 .filter(|(txid, _)| !all_wallet_tx_ids.contains(*txid))
3247 .map(|(_, tx)| tx)
3248 .collect();
3249
3250 for unknown_unconfirmed_tx in unknown_unconfirmed_txs {
3251 if unknown_unconfirmed_tx.timestamp.is_some_and(|t| {
3252 (utils::now().saturating_sub(t)) > NETWORK_PROPAGATION_GRACE_PERIOD.as_secs() as u32
3253 }) {
3254 self.persister
3255 .delete_payment_tx_data(&unknown_unconfirmed_tx.tx_id)?;
3256 info!(
3257 "Found an unknown unconfirmed tx and deleted it. Txid: {}",
3258 unknown_unconfirmed_tx.tx_id
3259 );
3260 } else {
3261 debug!(
3262 "Found an unknown unconfirmed tx that was inserted at {:?}. \
3263 Keeping it to allow propagation through the network. Txid: {}",
3264 unknown_unconfirmed_tx.timestamp, unknown_unconfirmed_tx.tx_id
3265 )
3266 }
3267 }
3268
3269 self.update_wallet_info().await?;
3270 Ok(())
3271 }
3272
3273 async fn update_wallet_info(&self) -> Result<()> {
3274 let asset_metadata: HashMap<String, AssetMetadata> = self
3275 .persister
3276 .list_asset_metadata()?
3277 .into_iter()
3278 .map(|am| (am.asset_id.clone(), am))
3279 .collect();
3280 let transactions = self.onchain_wallet.transactions().await?;
3281 let tx_ids = transactions
3282 .iter()
3283 .map(|tx| tx.txid.to_string())
3284 .collect::<Vec<_>>();
3285 let asset_balances = transactions
3286 .into_iter()
3287 .fold(BTreeMap::<AssetId, i64>::new(), |mut acc, tx| {
3288 tx.balance.into_iter().for_each(|(asset_id, balance)| {
3289 if tx.height.is_some() || balance < 0 {
3291 *acc.entry(asset_id).or_default() += balance;
3292 }
3293 });
3294 acc
3295 })
3296 .into_iter()
3297 .map(|(asset_id, balance)| {
3298 let asset_id = asset_id.to_hex();
3299 let balance_sat = balance.unsigned_abs();
3300 let maybe_asset_metadata = asset_metadata.get(&asset_id);
3301 AssetBalance {
3302 asset_id,
3303 balance_sat,
3304 name: maybe_asset_metadata.map(|am| am.name.clone()),
3305 ticker: maybe_asset_metadata.map(|am| am.ticker.clone()),
3306 balance: maybe_asset_metadata.map(|am| am.amount_from_sat(balance_sat)),
3307 }
3308 })
3309 .collect::<Vec<AssetBalance>>();
3310 let mut balance_sat = asset_balances
3311 .clone()
3312 .into_iter()
3313 .find(|ab| ab.asset_id.eq(&self.config.lbtc_asset_id()))
3314 .map_or(0, |ab| ab.balance_sat);
3315
3316 let mut pending_send_sat = 0;
3317 let mut pending_receive_sat = 0;
3318 let payments = self.persister.get_payments(&ListPaymentsRequest {
3319 states: Some(vec![
3320 PaymentState::Pending,
3321 PaymentState::RefundPending,
3322 PaymentState::WaitingFeeAcceptance,
3323 ]),
3324 ..Default::default()
3325 })?;
3326
3327 for payment in payments {
3328 let is_lbtc_asset_id = payment.details.is_lbtc_asset_id(self.config.network);
3329 match payment.payment_type {
3330 PaymentType::Send => match payment.details.get_refund_tx_amount_sat() {
3331 Some(refund_tx_amount_sat) => pending_receive_sat += refund_tx_amount_sat,
3332 None => {
3333 let total_sat = if is_lbtc_asset_id {
3334 payment.amount_sat + payment.fees_sat
3335 } else {
3336 payment.fees_sat
3337 };
3338 if let Some(tx_id) = payment.tx_id {
3339 if !tx_ids.contains(&tx_id) {
3340 debug!("Deducting {total_sat} sats from balance");
3341 balance_sat = balance_sat.saturating_sub(total_sat);
3342 }
3343 }
3344 pending_send_sat += total_sat
3345 }
3346 },
3347 PaymentType::Receive => {
3348 if is_lbtc_asset_id {
3349 pending_receive_sat += payment.amount_sat;
3350 }
3351 }
3352 }
3353 }
3354
3355 debug!("Onchain wallet balance: {balance_sat} sats");
3356 let info_response = WalletInfo {
3357 balance_sat,
3358 pending_send_sat,
3359 pending_receive_sat,
3360 fingerprint: self.onchain_wallet.fingerprint()?,
3361 pubkey: self.onchain_wallet.pubkey()?,
3362 asset_balances,
3363 };
3364 self.persister.set_wallet_info(&info_response)
3365 }
3366
3367 pub async fn list_payments(
3370 &self,
3371 req: &ListPaymentsRequest,
3372 ) -> Result<Vec<Payment>, PaymentError> {
3373 self.ensure_is_started().await?;
3374
3375 Ok(self.persister.get_payments(req)?)
3376 }
3377
3378 pub async fn get_payment(
3389 &self,
3390 req: &GetPaymentRequest,
3391 ) -> Result<Option<Payment>, PaymentError> {
3392 self.ensure_is_started().await?;
3393
3394 Ok(self.persister.get_payment_by_request(req)?)
3395 }
3396
3397 pub async fn fetch_payment_proposed_fees(
3402 &self,
3403 req: &FetchPaymentProposedFeesRequest,
3404 ) -> SdkResult<FetchPaymentProposedFeesResponse> {
3405 let chain_swap =
3406 self.persister
3407 .fetch_chain_swap_by_id(&req.swap_id)?
3408 .ok_or(SdkError::Generic {
3409 err: format!("Could not find Swap {}", req.swap_id),
3410 })?;
3411
3412 ensure_sdk!(
3413 chain_swap.state == WaitingFeeAcceptance,
3414 SdkError::Generic {
3415 err: "Payment is not WaitingFeeAcceptance".to_string()
3416 }
3417 );
3418
3419 let server_lockup_quote = self
3420 .swapper
3421 .get_zero_amount_chain_swap_quote(&req.swap_id)
3422 .await?;
3423
3424 let actual_payer_amount_sat =
3425 chain_swap
3426 .actual_payer_amount_sat
3427 .ok_or(SdkError::Generic {
3428 err: "No actual payer amount found when state is WaitingFeeAcceptance"
3429 .to_string(),
3430 })?;
3431 let fees_sat =
3432 actual_payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat;
3433
3434 Ok(FetchPaymentProposedFeesResponse {
3435 swap_id: req.swap_id.clone(),
3436 fees_sat,
3437 payer_amount_sat: actual_payer_amount_sat,
3438 receiver_amount_sat: actual_payer_amount_sat - fees_sat,
3439 })
3440 }
3441
3442 pub async fn accept_payment_proposed_fees(
3446 &self,
3447 req: &AcceptPaymentProposedFeesRequest,
3448 ) -> Result<(), PaymentError> {
3449 let FetchPaymentProposedFeesResponse {
3450 swap_id,
3451 fees_sat,
3452 payer_amount_sat,
3453 ..
3454 } = req.clone().response;
3455
3456 let chain_swap =
3457 self.persister
3458 .fetch_chain_swap_by_id(&swap_id)?
3459 .ok_or(SdkError::Generic {
3460 err: format!("Could not find Swap {}", swap_id),
3461 })?;
3462
3463 ensure_sdk!(
3464 chain_swap.state == WaitingFeeAcceptance,
3465 PaymentError::Generic {
3466 err: "Payment is not WaitingFeeAcceptance".to_string()
3467 }
3468 );
3469
3470 let server_lockup_quote = self
3471 .swapper
3472 .get_zero_amount_chain_swap_quote(&swap_id)
3473 .await?;
3474
3475 ensure_sdk!(
3476 fees_sat == payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat,
3477 PaymentError::InvalidOrExpiredFees
3478 );
3479
3480 self.persister
3481 .update_accepted_receiver_amount(&swap_id, Some(payer_amount_sat - fees_sat))?;
3482 self.swapper
3483 .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())
3484 .inspect_err(|e| {
3485 error!("Failed to accept zero-amount swap {swap_id} quote: {e} - trying to erase the accepted receiver amount...");
3486 let _ = self
3487 .persister
3488 .update_accepted_receiver_amount(&swap_id, None);
3489 }).await?;
3490 self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
3491 swap_id,
3492 to_state: Pending,
3493 ..Default::default()
3494 })
3495 }
3496
3497 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
3499 pub fn empty_wallet_cache(&self) -> Result<()> {
3500 let mut path = PathBuf::from(self.config.working_dir.clone());
3501 path.push(Into::<lwk_wollet::ElementsNetwork>::into(self.config.network).as_str());
3502 path.push("enc_cache");
3503
3504 std::fs::remove_dir_all(&path)?;
3505 std::fs::create_dir_all(path)?;
3506
3507 Ok(())
3508 }
3509
3510 pub async fn sync(&self, partial_sync: bool) -> SdkResult<()> {
3512 self.ensure_is_started().await?;
3513
3514 let t0 = Instant::now();
3515
3516 if let Err(err) = self.onchain_wallet.full_scan().await {
3517 error!("Failed to scan wallet: {err:?}");
3518 }
3519
3520 let is_first_sync = !self
3521 .persister
3522 .get_is_first_sync_complete()?
3523 .unwrap_or(false);
3524 match is_first_sync {
3525 true => {
3526 self.event_manager.pause_notifications();
3527 self.sync_payments_with_chain_data(partial_sync).await?;
3528 self.event_manager.resume_notifications();
3529 self.persister.set_is_first_sync_complete(true)?;
3530 }
3531 false => {
3532 self.sync_payments_with_chain_data(partial_sync).await?;
3533 }
3534 }
3535 let duration_ms = Instant::now().duration_since(t0).as_millis();
3536 info!("Synchronized (partial: {partial_sync}) with mempool and onchain data ({duration_ms} ms)");
3537
3538 self.notify_event_listeners(SdkEvent::Synced).await;
3539 Ok(())
3540 }
3541
3542 pub fn backup(&self, req: BackupRequest) -> Result<()> {
3549 let backup_path = req
3550 .backup_path
3551 .map(PathBuf::from)
3552 .unwrap_or(self.persister.get_default_backup_path());
3553 self.persister.backup(backup_path)
3554 }
3555
3556 pub fn restore(&self, req: RestoreRequest) -> Result<()> {
3563 let backup_path = req
3564 .backup_path
3565 .map(PathBuf::from)
3566 .unwrap_or(self.persister.get_default_backup_path());
3567 ensure_sdk!(
3568 backup_path.exists(),
3569 SdkError::generic("Backup file does not exist").into()
3570 );
3571 self.persister.restore_from_backup(backup_path)
3572 }
3573
3574 pub async fn prepare_lnurl_pay(
3605 &self,
3606 req: PrepareLnUrlPayRequest,
3607 ) -> Result<PrepareLnUrlPayResponse, LnUrlPayError> {
3608 let amount_msat = match req.amount {
3609 PayAmount::Drain => {
3610 let get_info_res = self
3611 .get_info()
3612 .await
3613 .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
3614 ensure_sdk!(
3615 get_info_res.wallet_info.pending_receive_sat == 0
3616 && get_info_res.wallet_info.pending_send_sat == 0,
3617 LnUrlPayError::Generic {
3618 err: "Cannot drain while there are pending payments".to_string(),
3619 }
3620 );
3621 let lbtc_pair = self
3622 .swapper
3623 .get_submarine_pairs()
3624 .await?
3625 .ok_or(PaymentError::PairsNotFound)?;
3626 let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
3627 let drain_amount_sat = get_info_res.wallet_info.balance_sat - drain_fees_sat;
3628 let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
3630 let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
3631 let invoice_amount_sat = utils::increment_invoice_amount_up_to_drain_amount(
3632 dummy_amount_sat,
3633 &lbtc_pair,
3634 drain_amount_sat,
3635 );
3636 lbtc_pair
3637 .limits
3638 .within(invoice_amount_sat)
3639 .map_err(|e| LnUrlPayError::Generic { err: e.message() })?;
3640 let pair_fees_sat = lbtc_pair.fees.total(invoice_amount_sat);
3642 ensure_sdk!(
3643 invoice_amount_sat + pair_fees_sat == drain_amount_sat,
3644 LnUrlPayError::Generic {
3645 err: "Cannot drain without leaving a remainder".to_string(),
3646 }
3647 );
3648
3649 invoice_amount_sat * 1000
3650 }
3651 PayAmount::Bitcoin {
3652 receiver_amount_sat,
3653 } => receiver_amount_sat * 1000,
3654 PayAmount::Asset { .. } => {
3655 return Err(LnUrlPayError::Generic {
3656 err: "Cannot send an asset to a Bitcoin address".to_string(),
3657 })
3658 }
3659 };
3660
3661 match validate_lnurl_pay(
3662 self.rest_client.as_ref(),
3663 amount_msat,
3664 &req.comment,
3665 &req.data,
3666 self.config.network.into(),
3667 req.validate_success_action_url,
3668 )
3669 .await?
3670 {
3671 ValidatedCallbackResponse::EndpointError { data } => {
3672 Err(LnUrlPayError::Generic { err: data.reason })
3673 }
3674 ValidatedCallbackResponse::EndpointSuccess { data } => {
3675 let prepare_response = self
3676 .prepare_send_payment(&PrepareSendRequest {
3677 destination: data.pr.clone(),
3678 amount: Some(req.amount),
3679 })
3680 .await
3681 .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
3682
3683 let destination = match prepare_response.destination {
3684 SendDestination::Bolt11 { invoice, .. } => SendDestination::Bolt11 {
3685 invoice,
3686 bip353_address: req.bip353_address,
3687 },
3688 SendDestination::LiquidAddress { address_data, .. } => {
3689 SendDestination::LiquidAddress {
3690 address_data,
3691 bip353_address: req.bip353_address,
3692 }
3693 }
3694 destination => destination,
3695 };
3696 let fees_sat = prepare_response
3697 .fees_sat
3698 .ok_or(PaymentError::InsufficientFunds)?;
3699
3700 Ok(PrepareLnUrlPayResponse {
3701 destination,
3702 fees_sat,
3703 data: req.data,
3704 comment: req.comment,
3705 success_action: data.success_action,
3706 })
3707 }
3708 }
3709 }
3710
3711 pub async fn lnurl_pay(
3724 &self,
3725 req: model::LnUrlPayRequest,
3726 ) -> Result<LnUrlPayResult, LnUrlPayError> {
3727 let prepare_response = req.prepare_response;
3728 let mut payment = self
3729 .send_payment(&SendPaymentRequest {
3730 prepare_response: PrepareSendResponse {
3731 destination: prepare_response.destination.clone(),
3732 fees_sat: Some(prepare_response.fees_sat),
3733 estimated_asset_fees: None,
3734 },
3735 use_asset_fees: None,
3736 })
3737 .await
3738 .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?
3739 .payment;
3740
3741 let maybe_sa_processed: Option<SuccessActionProcessed> = match prepare_response
3742 .success_action
3743 .clone()
3744 {
3745 Some(sa) => {
3746 match sa {
3747 SuccessAction::Aes { data } => {
3749 let PaymentDetails::Lightning {
3750 swap_id, preimage, ..
3751 } = &payment.details
3752 else {
3753 return Err(LnUrlPayError::Generic {
3754 err: format!("Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.", payment.details),
3755 });
3756 };
3757
3758 match preimage {
3759 Some(preimage_str) => {
3760 debug!(
3761 "Decrypting AES success action with preimage for Send Swap {}",
3762 swap_id
3763 );
3764 let preimage =
3765 sha256::Hash::from_str(preimage_str).map_err(|_| {
3766 LnUrlPayError::Generic {
3767 err: "Invalid preimage".to_string(),
3768 }
3769 })?;
3770 let preimage_arr = preimage.to_byte_array();
3771 let result = match (data, &preimage_arr).try_into() {
3772 Ok(data) => AesSuccessActionDataResult::Decrypted { data },
3773 Err(e) => AesSuccessActionDataResult::ErrorStatus {
3774 reason: e.to_string(),
3775 },
3776 };
3777 Some(SuccessActionProcessed::Aes { result })
3778 }
3779 None => {
3780 debug!("Preimage not yet available to decrypt AES success action for Send Swap {}", swap_id);
3781 None
3782 }
3783 }
3784 }
3785 SuccessAction::Message { data } => {
3786 Some(SuccessActionProcessed::Message { data })
3787 }
3788 SuccessAction::Url { data } => Some(SuccessActionProcessed::Url { data }),
3789 }
3790 }
3791 None => None,
3792 };
3793
3794 let description = payment
3795 .details
3796 .get_description()
3797 .or_else(|| extract_description_from_metadata(&prepare_response.data));
3798
3799 let lnurl_pay_domain = match prepare_response.data.ln_address {
3800 Some(_) => None,
3801 None => Some(prepare_response.data.domain),
3802 };
3803 if let (Some(tx_id), Some(destination)) =
3804 (payment.tx_id.clone(), payment.destination.clone())
3805 {
3806 self.persister
3807 .insert_or_update_payment_details(PaymentTxDetails {
3808 tx_id: tx_id.clone(),
3809 destination,
3810 description,
3811 lnurl_info: Some(LnUrlInfo {
3812 ln_address: prepare_response.data.ln_address,
3813 lnurl_pay_comment: prepare_response.comment,
3814 lnurl_pay_domain,
3815 lnurl_pay_metadata: Some(prepare_response.data.metadata_str),
3816 lnurl_pay_success_action: maybe_sa_processed.clone(),
3817 lnurl_pay_unprocessed_success_action: prepare_response.success_action,
3818 lnurl_withdraw_endpoint: None,
3819 }),
3820 bip353_address: None,
3821 asset_fees: None,
3822 })?;
3823 payment = self.persister.get_payment(&tx_id)?.unwrap_or(payment);
3825 }
3826
3827 Ok(LnUrlPayResult::EndpointSuccess {
3828 data: model::LnUrlPaySuccessData {
3829 payment,
3830 success_action: maybe_sa_processed,
3831 },
3832 })
3833 }
3834
3835 pub async fn lnurl_withdraw(
3842 &self,
3843 req: LnUrlWithdrawRequest,
3844 ) -> Result<LnUrlWithdrawResult, LnUrlWithdrawError> {
3845 let prepare_response = self
3846 .prepare_receive_payment(&{
3847 PrepareReceiveRequest {
3848 payment_method: PaymentMethod::Lightning,
3849 amount: Some(ReceiveAmount::Bitcoin {
3850 payer_amount_sat: req.amount_msat / 1_000,
3851 }),
3852 }
3853 })
3854 .await?;
3855 let receive_res = self
3856 .receive_payment(&ReceivePaymentRequest {
3857 prepare_response,
3858 description: req.description.clone(),
3859 use_description_hash: Some(false),
3860 })
3861 .await?;
3862
3863 let Ok(invoice) = parse_invoice(&receive_res.destination) else {
3864 return Err(LnUrlWithdrawError::Generic {
3865 err: "Received unexpected output from receive request".to_string(),
3866 });
3867 };
3868
3869 let res =
3870 validate_lnurl_withdraw(self.rest_client.as_ref(), req.data.clone(), invoice.clone())
3871 .await?;
3872 if let LnUrlWithdrawResult::Ok { data: _ } = res {
3873 if let Some(ReceiveSwap {
3874 claim_tx_id: Some(tx_id),
3875 ..
3876 }) = self
3877 .persister
3878 .fetch_receive_swap_by_invoice(&invoice.bolt11)?
3879 {
3880 self.persister
3881 .insert_or_update_payment_details(PaymentTxDetails {
3882 tx_id,
3883 destination: receive_res.destination,
3884 description: req.description,
3885 lnurl_info: Some(LnUrlInfo {
3886 lnurl_withdraw_endpoint: Some(req.data.callback),
3887 ..Default::default()
3888 }),
3889 bip353_address: None,
3890 asset_fees: None,
3891 })?;
3892 }
3893 }
3894 Ok(res)
3895 }
3896
3897 pub async fn lnurl_auth(
3903 &self,
3904 req_data: LnUrlAuthRequestData,
3905 ) -> Result<LnUrlCallbackStatus, LnUrlAuthError> {
3906 Ok(perform_lnurl_auth(
3907 self.rest_client.as_ref(),
3908 &req_data,
3909 &SdkLnurlAuthSigner::new(self.signer.clone()),
3910 )
3911 .await?)
3912 }
3913
3914 pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> {
3922 info!("Registering for webhook notifications");
3923 self.persister.set_webhook_url(webhook_url)?;
3924 Ok(())
3925 }
3926
3927 pub async fn unregister_webhook(&self) -> SdkResult<()> {
3934 info!("Unregistering for webhook notifications");
3935 self.persister.remove_webhook_url()?;
3936 Ok(())
3937 }
3938
3939 pub async fn fetch_fiat_rates(&self) -> Result<Vec<Rate>, SdkError> {
3941 self.fiat_api.fetch_fiat_rates().await.map_err(Into::into)
3942 }
3943
3944 pub async fn list_fiat_currencies(&self) -> Result<Vec<FiatCurrency>, SdkError> {
3947 self.fiat_api
3948 .list_fiat_currencies()
3949 .await
3950 .map_err(Into::into)
3951 }
3952
3953 pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
3955 Ok(self.bitcoin_chain_service.recommended_fees().await?)
3956 }
3957
3958 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
3959 pub fn default_config(
3961 network: LiquidNetwork,
3962 breez_api_key: Option<String>,
3963 ) -> Result<Config, SdkError> {
3964 let config = match network {
3965 LiquidNetwork::Mainnet => Config::mainnet(breez_api_key),
3966 LiquidNetwork::Testnet => Config::testnet(breez_api_key),
3967 LiquidNetwork::Regtest => Config::regtest(),
3968 };
3969
3970 Ok(config)
3971 }
3972
3973 pub async fn parse(&self, input: &str) -> Result<InputType, PaymentError> {
3977 let external_parsers = &self.external_input_parsers;
3978 let input_type =
3979 parse_with_rest_client(self.rest_client.as_ref(), input, Some(external_parsers))
3980 .await
3981 .map_err(|e| PaymentError::generic(&e.to_string()))?;
3982
3983 let res = match input_type {
3984 InputType::LiquidAddress { ref address } => match &address.asset_id {
3985 Some(asset_id) if asset_id.ne(&self.config.lbtc_asset_id()) => {
3986 let asset_metadata = self.persister.get_asset_metadata(asset_id)?.ok_or(
3987 PaymentError::AssetError {
3988 err: format!("Asset {asset_id} is not supported"),
3989 },
3990 )?;
3991 let mut address = address.clone();
3992 address.set_amount_precision(asset_metadata.precision.into());
3993 InputType::LiquidAddress { address }
3994 }
3995 _ => input_type,
3996 },
3997 _ => input_type,
3998 };
3999 Ok(res)
4000 }
4001
4002 pub fn parse_invoice(input: &str) -> Result<LNInvoice, PaymentError> {
4004 parse_invoice(input).map_err(|e| PaymentError::invalid_invoice(&e.to_string()))
4005 }
4006
4007 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4031 pub fn init_logging(log_dir: &str, app_logger: Option<Box<dyn log::Log>>) -> Result<()> {
4032 crate::logger::init_logging(log_dir, app_logger)
4033 }
4034}
4035
4036fn extract_description_from_metadata(request_data: &LnUrlPayRequestData) -> Option<String> {
4038 let metadata = request_data.metadata_vec().ok()?;
4039 metadata
4040 .iter()
4041 .find(|item| item.key == "text/plain")
4042 .map(|item| {
4043 info!("Extracted payment description: '{}'", item.value);
4044 item.value.clone()
4045 })
4046}
4047
4048#[cfg(test)]
4049mod tests {
4050 use std::str::FromStr;
4051 use std::time::Duration;
4052
4053 use anyhow::{anyhow, Result};
4054 use boltz_client::{
4055 boltz::{self, TransactionInfo},
4056 swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates},
4057 };
4058 use lwk_wollet::hashes::hex::DisplayHex as _;
4059 use sdk_common::utils::Arc;
4060 use tokio_with_wasm::alias as tokio;
4061
4062 use crate::chain_swap::ESTIMATED_BTC_LOCKUP_TX_VSIZE;
4063 use crate::test_utils::chain_swap::{
4064 TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX, TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX,
4065 TEST_LIQUID_OUTGOING_USER_LOCKUP_TX,
4066 };
4067 use crate::test_utils::swapper::ZeroAmountSwapMockConfig;
4068 use crate::test_utils::wallet::TEST_LIQUID_RECEIVE_LOCKUP_TX;
4069 use crate::{
4070 bitcoin, elements,
4071 model::{BtcHistory, Direction, LBtcHistory, PaymentState, Swap},
4072 sdk::LiquidSdk,
4073 test_utils::{
4074 chain::{MockBitcoinChainService, MockLiquidChainService},
4075 chain_swap::{new_chain_swap, TEST_BITCOIN_INCOMING_USER_LOCKUP_TX},
4076 persist::{create_persister, new_receive_swap, new_send_swap},
4077 sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services},
4078 status_stream::MockStatusStream,
4079 swapper::MockSwapper,
4080 },
4081 };
4082 use paste::paste;
4083
4084 #[cfg(feature = "browser-tests")]
4085 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
4086
4087 struct NewSwapArgs {
4088 direction: Direction,
4089 accepts_zero_conf: bool,
4090 initial_payment_state: Option<PaymentState>,
4091 receiver_amount_sat: Option<u64>,
4092 user_lockup_tx_id: Option<String>,
4093 zero_amount: bool,
4094 set_actual_payer_amount: bool,
4095 }
4096
4097 impl Default for NewSwapArgs {
4098 fn default() -> Self {
4099 Self {
4100 accepts_zero_conf: false,
4101 initial_payment_state: None,
4102 direction: Direction::Outgoing,
4103 receiver_amount_sat: None,
4104 user_lockup_tx_id: None,
4105 zero_amount: false,
4106 set_actual_payer_amount: false,
4107 }
4108 }
4109 }
4110
4111 impl NewSwapArgs {
4112 pub fn set_direction(mut self, direction: Direction) -> Self {
4113 self.direction = direction;
4114 self
4115 }
4116
4117 pub fn set_accepts_zero_conf(mut self, accepts_zero_conf: bool) -> Self {
4118 self.accepts_zero_conf = accepts_zero_conf;
4119 self
4120 }
4121
4122 pub fn set_receiver_amount_sat(mut self, receiver_amount_sat: Option<u64>) -> Self {
4123 self.receiver_amount_sat = receiver_amount_sat;
4124 self
4125 }
4126
4127 pub fn set_user_lockup_tx_id(mut self, user_lockup_tx_id: Option<String>) -> Self {
4128 self.user_lockup_tx_id = user_lockup_tx_id;
4129 self
4130 }
4131
4132 pub fn set_initial_payment_state(mut self, payment_state: PaymentState) -> Self {
4133 self.initial_payment_state = Some(payment_state);
4134 self
4135 }
4136
4137 pub fn set_zero_amount(mut self, zero_amount: bool) -> Self {
4138 self.zero_amount = zero_amount;
4139 self
4140 }
4141
4142 pub fn set_set_actual_payer_amount(mut self, set_actual_payer_amount: bool) -> Self {
4143 self.set_actual_payer_amount = set_actual_payer_amount;
4144 self
4145 }
4146 }
4147
4148 macro_rules! trigger_swap_update {
4149 (
4150 $type:literal,
4151 $args:expr,
4152 $persister:expr,
4153 $status_stream:expr,
4154 $status:expr,
4155 $transaction:expr,
4156 $zero_conf_rejected:expr
4157 ) => {{
4158 let swap = match $type {
4159 "chain" => {
4160 let swap = new_chain_swap(
4161 $args.direction,
4162 $args.initial_payment_state,
4163 $args.accepts_zero_conf,
4164 $args.user_lockup_tx_id,
4165 $args.zero_amount,
4166 $args.set_actual_payer_amount,
4167 $args.receiver_amount_sat,
4168 );
4169 $persister.insert_or_update_chain_swap(&swap).unwrap();
4170 Swap::Chain(swap)
4171 }
4172 "send" => {
4173 let swap =
4174 new_send_swap($args.initial_payment_state, $args.receiver_amount_sat);
4175 $persister.insert_or_update_send_swap(&swap).unwrap();
4176 Swap::Send(swap)
4177 }
4178 "receive" => {
4179 let swap =
4180 new_receive_swap($args.initial_payment_state, $args.receiver_amount_sat);
4181 $persister.insert_or_update_receive_swap(&swap).unwrap();
4182 Swap::Receive(swap)
4183 }
4184 _ => panic!(),
4185 };
4186
4187 $status_stream
4188 .clone()
4189 .send_mock_update(boltz::SwapStatus {
4190 id: swap.id(),
4191 status: $status.to_string(),
4192 transaction: $transaction,
4193 zero_conf_rejected: $zero_conf_rejected,
4194 ..Default::default()
4195 })
4196 .await
4197 .unwrap();
4198
4199 paste! {
4200 $persister.[<fetch _ $type _swap_by_id>](&swap.id())
4201 .unwrap()
4202 .ok_or(anyhow!("Could not retrieve {} swap", $type))
4203 .unwrap()
4204 }
4205 }};
4206 }
4207
4208 #[sdk_macros::async_test_all]
4209 async fn test_receive_swap_update_tracking() -> Result<()> {
4210 create_persister!(persister);
4211 let swapper = Arc::new(MockSwapper::default());
4212 let status_stream = Arc::new(MockStatusStream::new());
4213 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
4214 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
4215
4216 let sdk = new_liquid_sdk_with_chain_services(
4217 persister.clone(),
4218 swapper.clone(),
4219 status_stream.clone(),
4220 liquid_chain_service.clone(),
4221 bitcoin_chain_service.clone(),
4222 None,
4223 )
4224 .await?;
4225
4226 LiquidSdk::track_swap_updates(&sdk);
4227
4228 tokio::spawn(async move {
4230 let unrecoverable_states: [RevSwapStates; 4] = [
4232 RevSwapStates::SwapExpired,
4233 RevSwapStates::InvoiceExpired,
4234 RevSwapStates::TransactionFailed,
4235 RevSwapStates::TransactionRefunded,
4236 ];
4237
4238 for status in unrecoverable_states {
4239 let persisted_swap = trigger_swap_update!(
4240 "receive",
4241 NewSwapArgs::default(),
4242 persister,
4243 status_stream,
4244 status,
4245 None,
4246 None
4247 );
4248 assert_eq!(persisted_swap.state, PaymentState::Failed);
4249 }
4250
4251 for status in [
4254 RevSwapStates::TransactionMempool,
4255 RevSwapStates::TransactionConfirmed,
4256 ] {
4257 let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
4258 let mock_tx_id = mock_tx.txid();
4259 let height = (serde_json::to_string(&status).unwrap()
4260 == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
4261 as i32;
4262 liquid_chain_service.set_history(vec![LBtcHistory {
4263 txid: mock_tx_id,
4264 height,
4265 }]);
4266
4267 let persisted_swap = trigger_swap_update!(
4268 "receive",
4269 NewSwapArgs::default(),
4270 persister,
4271 status_stream,
4272 status,
4273 Some(TransactionInfo {
4274 id: mock_tx_id.to_string(),
4275 hex: Some(
4276 lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
4277 ),
4278 eta: None,
4279 }),
4280 None
4281 );
4282 assert!(persisted_swap.claim_tx_id.is_some());
4283 }
4284
4285 for status in [
4288 RevSwapStates::TransactionMempool,
4289 RevSwapStates::TransactionConfirmed,
4290 ] {
4291 let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
4292 let mock_tx_id = mock_tx.txid();
4293 let height = (serde_json::to_string(&status).unwrap()
4294 == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
4295 as i32;
4296 liquid_chain_service.set_history(vec![LBtcHistory {
4297 txid: mock_tx_id,
4298 height,
4299 }]);
4300
4301 let persisted_swap = trigger_swap_update!(
4302 "receive",
4303 NewSwapArgs::default().set_receiver_amount_sat(Some(1000)),
4304 persister,
4305 status_stream,
4306 status,
4307 Some(TransactionInfo {
4308 id: mock_tx_id.to_string(),
4309 hex: Some(
4310 lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
4311 ),
4312 eta: None
4313 }),
4314 None
4315 );
4316 assert!(persisted_swap.claim_tx_id.is_none());
4317 }
4318 })
4319 .await
4320 .unwrap();
4321
4322 Ok(())
4323 }
4324
4325 #[sdk_macros::async_test_all]
4326 async fn test_send_swap_update_tracking() -> Result<()> {
4327 create_persister!(persister);
4328 let swapper = Arc::new(MockSwapper::default());
4329 let status_stream = Arc::new(MockStatusStream::new());
4330
4331 let sdk = Arc::new(
4332 new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?,
4333 );
4334
4335 LiquidSdk::track_swap_updates(&sdk);
4336
4337 tokio::spawn(async move {
4339 let unrecoverable_states: [SubSwapStates; 3] = [
4341 SubSwapStates::TransactionLockupFailed,
4342 SubSwapStates::InvoiceFailedToPay,
4343 SubSwapStates::SwapExpired,
4344 ];
4345
4346 for status in unrecoverable_states {
4347 let persisted_swap = trigger_swap_update!(
4348 "send",
4349 NewSwapArgs::default(),
4350 persister,
4351 status_stream,
4352 status,
4353 None,
4354 None
4355 );
4356 assert_eq!(persisted_swap.state, PaymentState::Failed);
4357 }
4358
4359 let persisted_swap = trigger_swap_update!(
4362 "send",
4363 NewSwapArgs::default(),
4364 persister,
4365 status_stream,
4366 SubSwapStates::TransactionClaimPending,
4367 None,
4368 None
4369 );
4370 assert_eq!(persisted_swap.state, PaymentState::Complete);
4371 assert!(persisted_swap.preimage.is_some());
4372 })
4373 .await
4374 .unwrap();
4375
4376 Ok(())
4377 }
4378
4379 #[sdk_macros::async_test_all]
4380 async fn test_chain_swap_update_tracking() -> Result<()> {
4381 create_persister!(persister);
4382 let swapper = Arc::new(MockSwapper::default());
4383 let status_stream = Arc::new(MockStatusStream::new());
4384 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
4385 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
4386
4387 let sdk = new_liquid_sdk_with_chain_services(
4388 persister.clone(),
4389 swapper.clone(),
4390 status_stream.clone(),
4391 liquid_chain_service.clone(),
4392 bitcoin_chain_service.clone(),
4393 None,
4394 )
4395 .await?;
4396
4397 LiquidSdk::track_swap_updates(&sdk);
4398
4399 tokio::spawn(async move {
4401 let trigger_failed: [ChainSwapStates; 3] = [
4402 ChainSwapStates::TransactionFailed,
4403 ChainSwapStates::SwapExpired,
4404 ChainSwapStates::TransactionRefunded,
4405 ];
4406
4407 for direction in [Direction::Incoming, Direction::Outgoing] {
4409 for status in &trigger_failed {
4411 let persisted_swap = trigger_swap_update!(
4412 "chain",
4413 NewSwapArgs::default().set_direction(direction),
4414 persister,
4415 status_stream,
4416 status,
4417 None,
4418 None
4419 );
4420 assert_eq!(persisted_swap.state, PaymentState::Failed);
4421 }
4422
4423 let (mock_user_lockup_tx_hex, mock_user_lockup_tx_id) = match direction {
4424 Direction::Outgoing => {
4425 let tx = TEST_LIQUID_OUTGOING_USER_LOCKUP_TX.clone();
4426 (
4427 lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
4428 tx.txid().to_string(),
4429 )
4430 }
4431 Direction::Incoming => {
4432 let tx = TEST_BITCOIN_INCOMING_USER_LOCKUP_TX.clone();
4433 (
4434 sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
4435 tx.txid().to_string(),
4436 )
4437 }
4438 };
4439
4440 let (mock_server_lockup_tx_hex, mock_server_lockup_tx_id) = match direction {
4441 Direction::Incoming => {
4442 let tx = TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX.clone();
4443 (
4444 lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
4445 tx.txid().to_string(),
4446 )
4447 }
4448 Direction::Outgoing => {
4449 let tx = TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX.clone();
4450 (
4451 sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
4452 tx.txid().to_string(),
4453 )
4454 }
4455 };
4456
4457 for user_lockup_tx_id in &[None, Some(mock_user_lockup_tx_id.clone())] {
4461 if let Some(user_lockup_tx_id) = user_lockup_tx_id {
4462 match direction {
4463 Direction::Incoming => {
4464 bitcoin_chain_service.set_history(vec![BtcHistory {
4465 txid: bitcoin::Txid::from_str(user_lockup_tx_id).unwrap(),
4466 height: 0,
4467 }]);
4468 }
4469 Direction::Outgoing => {
4470 liquid_chain_service.set_history(vec![LBtcHistory {
4471 txid: elements::Txid::from_str(user_lockup_tx_id).unwrap(),
4472 height: 0,
4473 }]);
4474 }
4475 }
4476 }
4477 let persisted_swap = trigger_swap_update!(
4478 "chain",
4479 NewSwapArgs::default()
4480 .set_direction(direction)
4481 .set_initial_payment_state(PaymentState::Pending)
4482 .set_user_lockup_tx_id(user_lockup_tx_id.clone()),
4483 persister,
4484 status_stream,
4485 ChainSwapStates::TransactionLockupFailed,
4486 None,
4487 None
4488 );
4489 let expected_state = if user_lockup_tx_id.is_some() {
4490 match direction {
4491 Direction::Incoming => PaymentState::Refundable,
4492 Direction::Outgoing => PaymentState::RefundPending,
4493 }
4494 } else {
4495 PaymentState::Failed
4496 };
4497 assert_eq!(persisted_swap.state, expected_state);
4498 }
4499
4500 for status in [
4503 ChainSwapStates::TransactionMempool,
4504 ChainSwapStates::TransactionConfirmed,
4505 ] {
4506 if direction == Direction::Incoming {
4507 bitcoin_chain_service.set_history(vec![BtcHistory {
4508 txid: bitcoin::Txid::from_str(&mock_user_lockup_tx_id).unwrap(),
4509 height: 0,
4510 }]);
4511 bitcoin_chain_service.set_transactions(&[&mock_user_lockup_tx_hex]);
4512 }
4513 let persisted_swap = trigger_swap_update!(
4514 "chain",
4515 NewSwapArgs::default().set_direction(direction),
4516 persister,
4517 status_stream,
4518 status,
4519 Some(TransactionInfo {
4520 id: mock_user_lockup_tx_id.clone(),
4521 hex: Some(mock_user_lockup_tx_hex.clone()),
4522 eta: None
4523 }), Some(true) );
4526 assert_eq!(
4527 persisted_swap.user_lockup_tx_id,
4528 Some(mock_user_lockup_tx_id.clone())
4529 );
4530 assert!(!persisted_swap.accept_zero_conf);
4531 }
4532
4533 for accepts_zero_conf in [false, true] {
4539 let persisted_swap = trigger_swap_update!(
4540 "chain",
4541 NewSwapArgs::default()
4542 .set_direction(direction)
4543 .set_accepts_zero_conf(accepts_zero_conf)
4544 .set_set_actual_payer_amount(true),
4545 persister,
4546 status_stream,
4547 ChainSwapStates::TransactionServerMempool,
4548 Some(TransactionInfo {
4549 id: mock_server_lockup_tx_id.clone(),
4550 hex: Some(mock_server_lockup_tx_hex.clone()),
4551 eta: None,
4552 }),
4553 None
4554 );
4555 match accepts_zero_conf {
4556 false => {
4557 assert_eq!(persisted_swap.state, PaymentState::Pending);
4558 assert!(persisted_swap.server_lockup_tx_id.is_some());
4559 }
4560 true => {
4561 assert_eq!(persisted_swap.state, PaymentState::Pending);
4562 assert!(persisted_swap.claim_tx_id.is_some());
4563 }
4564 };
4565 }
4566
4567 let persisted_swap = trigger_swap_update!(
4570 "chain",
4571 NewSwapArgs::default()
4572 .set_direction(direction)
4573 .set_set_actual_payer_amount(true),
4574 persister,
4575 status_stream,
4576 ChainSwapStates::TransactionServerConfirmed,
4577 Some(TransactionInfo {
4578 id: mock_server_lockup_tx_id,
4579 hex: Some(mock_server_lockup_tx_hex),
4580 eta: None,
4581 }),
4582 None
4583 );
4584 assert_eq!(persisted_swap.state, PaymentState::Pending);
4585 assert!(persisted_swap.claim_tx_id.is_some());
4586 }
4587
4588 let persisted_swap = trigger_swap_update!(
4591 "chain",
4592 NewSwapArgs::default().set_direction(Direction::Outgoing),
4593 persister,
4594 status_stream,
4595 ChainSwapStates::Created,
4596 None,
4597 None
4598 );
4599 assert_eq!(persisted_swap.state, PaymentState::Pending);
4600 assert!(persisted_swap.user_lockup_tx_id.is_some());
4601 })
4602 .await
4603 .unwrap();
4604
4605 Ok(())
4606 }
4607
4608 #[sdk_macros::async_test_all]
4609 async fn test_zero_amount_chain_swap_zero_leeway() -> Result<()> {
4610 let user_lockup_sat = 50_000;
4611
4612 create_persister!(persister);
4613 let swapper = Arc::new(MockSwapper::new());
4614 let status_stream = Arc::new(MockStatusStream::new());
4615 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
4616 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
4617
4618 let sdk = new_liquid_sdk_with_chain_services(
4619 persister.clone(),
4620 swapper.clone(),
4621 status_stream.clone(),
4622 liquid_chain_service.clone(),
4623 bitcoin_chain_service.clone(),
4624 None,
4625 )
4626 .await?;
4627
4628 LiquidSdk::track_swap_updates(&sdk);
4629
4630 tokio::spawn(async move {
4632 for fee_increase in [0, 1] {
4636 swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
4637 user_lockup_sat,
4638 onchain_fee_increase_sat: fee_increase,
4639 });
4640 bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
4641 let persisted_swap = trigger_swap_update!(
4642 "chain",
4643 NewSwapArgs::default()
4644 .set_direction(Direction::Incoming)
4645 .set_accepts_zero_conf(false)
4646 .set_zero_amount(true),
4647 persister,
4648 status_stream,
4649 ChainSwapStates::TransactionLockupFailed,
4650 None,
4651 None
4652 );
4653 match fee_increase {
4654 0 => {
4655 assert_eq!(persisted_swap.state, PaymentState::Created);
4656 }
4657 1 => {
4658 assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
4659 }
4660 _ => panic!("Unexpected fee_increase"),
4661 }
4662 }
4663 })
4664 .await?;
4665
4666 Ok(())
4667 }
4668
4669 #[sdk_macros::async_test_all]
4670 async fn test_zero_amount_chain_swap_with_leeway() -> Result<()> {
4671 let user_lockup_sat = 50_000;
4672 let onchain_fee_rate_leeway_sat_per_vbyte = 5;
4673
4674 create_persister!(persister);
4675 let swapper = Arc::new(MockSwapper::new());
4676 let status_stream = Arc::new(MockStatusStream::new());
4677 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
4678 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
4679
4680 let sdk = new_liquid_sdk_with_chain_services(
4681 persister.clone(),
4682 swapper.clone(),
4683 status_stream.clone(),
4684 liquid_chain_service.clone(),
4685 bitcoin_chain_service.clone(),
4686 Some(onchain_fee_rate_leeway_sat_per_vbyte),
4687 )
4688 .await?;
4689
4690 LiquidSdk::track_swap_updates(&sdk);
4691
4692 let max_fee_increase_for_auto_accept_sat =
4693 onchain_fee_rate_leeway_sat_per_vbyte as u64 * ESTIMATED_BTC_LOCKUP_TX_VSIZE;
4694
4695 tokio::spawn(async move {
4697 for fee_increase in [
4701 max_fee_increase_for_auto_accept_sat,
4702 max_fee_increase_for_auto_accept_sat + 1,
4703 ] {
4704 swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
4705 user_lockup_sat,
4706 onchain_fee_increase_sat: fee_increase,
4707 });
4708 bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
4709 let persisted_swap = trigger_swap_update!(
4710 "chain",
4711 NewSwapArgs::default()
4712 .set_direction(Direction::Incoming)
4713 .set_accepts_zero_conf(false)
4714 .set_zero_amount(true),
4715 persister,
4716 status_stream,
4717 ChainSwapStates::TransactionLockupFailed,
4718 None,
4719 None
4720 );
4721 match fee_increase {
4722 val if val == max_fee_increase_for_auto_accept_sat => {
4723 assert_eq!(persisted_swap.state, PaymentState::Created);
4724 }
4725 val if val == (max_fee_increase_for_auto_accept_sat + 1) => {
4726 assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
4727 }
4728 _ => panic!("Unexpected fee_increase"),
4729 }
4730 }
4731 })
4732 .await?;
4733
4734 Ok(())
4735 }
4736
4737 #[sdk_macros::async_test_all]
4738 async fn test_background_tasks() -> Result<()> {
4739 create_persister!(persister);
4740 let swapper = Arc::new(MockSwapper::new());
4741 let status_stream = Arc::new(MockStatusStream::new());
4742 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
4743 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
4744
4745 let sdk = new_liquid_sdk_with_chain_services(
4746 persister.clone(),
4747 swapper.clone(),
4748 status_stream.clone(),
4749 liquid_chain_service.clone(),
4750 bitcoin_chain_service.clone(),
4751 None,
4752 )
4753 .await?;
4754
4755 sdk.start().await?;
4756
4757 tokio::time::sleep(Duration::from_secs(3)).await;
4758
4759 sdk.disconnect().await?;
4760
4761 Ok(())
4762 }
4763}