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