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 an 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(
1218 &self,
1219 req: &PrepareSendRequest,
1220 ) -> Result<PrepareSendResponse, PaymentError> {
1221 self.ensure_is_started().await?;
1222
1223 let get_info_res = self.get_info().await?;
1224 let fees_sat;
1225 let estimated_asset_fees;
1226 let receiver_amount_sat;
1227 let asset_id;
1228 let payment_destination;
1229
1230 match self.parse(&req.destination).await {
1231 Ok(InputType::LiquidAddress {
1232 address: mut liquid_address_data,
1233 }) => {
1234 let amount = match (
1235 liquid_address_data.amount,
1236 liquid_address_data.amount_sat,
1237 liquid_address_data.asset_id,
1238 req.amount.clone(),
1239 ) {
1240 (Some(amount), Some(amount_sat), Some(asset_id), None) => {
1241 if asset_id.eq(&self.config.lbtc_asset_id()) {
1242 PayAmount::Bitcoin {
1243 receiver_amount_sat: amount_sat,
1244 }
1245 } else {
1246 PayAmount::Asset {
1247 asset_id,
1248 receiver_amount: amount,
1249 estimate_asset_fees: None,
1250 }
1251 }
1252 }
1253 (_, Some(amount_sat), None, None) => PayAmount::Bitcoin {
1254 receiver_amount_sat: amount_sat,
1255 },
1256 (_, _, _, Some(amount)) => amount,
1257 _ => {
1258 return Err(PaymentError::AmountMissing {
1259 err: "Amount must be set when paying to a Liquid address".to_string(),
1260 });
1261 }
1262 };
1263
1264 ensure_sdk!(
1265 liquid_address_data.network == self.config.network.into(),
1266 PaymentError::InvalidNetwork {
1267 err: format!(
1268 "Cannot send payment from {} to {}",
1269 Into::<sdk_common::bitcoin::Network>::into(self.config.network),
1270 liquid_address_data.network
1271 )
1272 }
1273 );
1274
1275 (
1276 asset_id,
1277 receiver_amount_sat,
1278 fees_sat,
1279 estimated_asset_fees,
1280 ) = match amount {
1281 PayAmount::Drain => {
1282 ensure_sdk!(
1283 get_info_res.wallet_info.pending_receive_sat == 0
1284 && get_info_res.wallet_info.pending_send_sat == 0,
1285 PaymentError::Generic {
1286 err: "Cannot drain while there are pending payments".to_string(),
1287 }
1288 );
1289 let drain_fees_sat = self
1290 .estimate_drain_tx_fee(None, Some(&liquid_address_data.address))
1291 .await?;
1292 let drain_amount_sat =
1293 get_info_res.wallet_info.balance_sat - drain_fees_sat;
1294 info!("Drain amount: {drain_amount_sat} sat");
1295 (
1296 self.config.lbtc_asset_id(),
1297 drain_amount_sat,
1298 Some(drain_fees_sat),
1299 None,
1300 )
1301 }
1302 PayAmount::Bitcoin {
1303 receiver_amount_sat,
1304 } => {
1305 let asset_id = self.config.lbtc_asset_id();
1306 let fees_sat = self
1307 .estimate_onchain_tx_or_drain_tx_fee(
1308 receiver_amount_sat,
1309 &liquid_address_data.address,
1310 &asset_id,
1311 )
1312 .await?;
1313 (asset_id, receiver_amount_sat, Some(fees_sat), None)
1314 }
1315 PayAmount::Asset {
1316 asset_id,
1317 receiver_amount,
1318 estimate_asset_fees,
1319 } => {
1320 let estimate_asset_fees = estimate_asset_fees.unwrap_or(false);
1321 let asset_metadata = self.persister.get_asset_metadata(&asset_id)?.ok_or(
1322 PaymentError::AssetError {
1323 err: format!("Asset {asset_id} is not supported"),
1324 },
1325 )?;
1326 let receiver_amount_sat = asset_metadata.amount_to_sat(receiver_amount);
1327 let fees_sat_res = self
1328 .estimate_onchain_tx_or_drain_tx_fee(
1329 receiver_amount_sat,
1330 &liquid_address_data.address,
1331 &asset_id,
1332 )
1333 .await;
1334 let asset_fees = if estimate_asset_fees {
1335 self.payjoin_service
1336 .estimate_payjoin_tx_fee(&asset_id, receiver_amount_sat)
1337 .await
1338 .inspect_err(|e| debug!("Error estimating payjoin tx: {e}"))
1339 .ok()
1340 } else {
1341 None
1342 };
1343 let (fees_sat, asset_fees) = match (fees_sat_res, asset_fees) {
1344 (Ok(fees_sat), _) => (Some(fees_sat), asset_fees),
1345 (Err(e), Some(asset_fees)) => {
1346 debug!(
1347 "Error estimating onchain tx, but returning payjoin fees: {e}"
1348 );
1349 (None, Some(asset_fees))
1350 }
1351 (Err(e), None) => return Err(e),
1352 };
1353 (asset_id, receiver_amount_sat, fees_sat, asset_fees)
1354 }
1355 };
1356
1357 liquid_address_data.amount_sat = Some(receiver_amount_sat);
1358 liquid_address_data.asset_id = Some(asset_id.clone());
1359 payment_destination = SendDestination::LiquidAddress {
1360 address_data: liquid_address_data,
1361 bip353_address: None,
1362 };
1363 }
1364 Ok(InputType::Bolt11 { invoice }) => {
1365 self.ensure_send_is_not_self_transfer(&invoice.bolt11)?;
1366 self.validate_bolt11_invoice(&invoice.bolt11)?;
1367
1368 let invoice_amount_sat = invoice.amount_msat.ok_or(
1369 PaymentError::amount_missing("Expected invoice with an amount"),
1370 )? / 1000;
1371
1372 if let Some(PayAmount::Bitcoin {
1373 receiver_amount_sat: amount_sat,
1374 }) = req.amount
1375 {
1376 ensure_sdk!(
1377 invoice_amount_sat == amount_sat,
1378 PaymentError::Generic {
1379 err: "Receiver amount and invoice amount do not match".to_string()
1380 }
1381 );
1382 }
1383
1384 let lbtc_pair = self.validate_submarine_pairs(invoice_amount_sat).await?;
1385 let mrh_address = self
1386 .swapper
1387 .check_for_mrh(&invoice.bolt11)
1388 .await?
1389 .map(|(address, _)| address);
1390 asset_id = self.config.lbtc_asset_id();
1391 estimated_asset_fees = None;
1392 (receiver_amount_sat, fees_sat) = match (mrh_address.clone(), req.amount.clone()) {
1393 (Some(lbtc_address), Some(PayAmount::Drain)) => {
1394 let drain_fees_sat = self
1398 .estimate_drain_tx_fee(None, Some(&lbtc_address))
1399 .await?;
1400 let drain_amount_sat =
1401 get_info_res.wallet_info.balance_sat - drain_fees_sat;
1402 (drain_amount_sat, Some(drain_fees_sat))
1403 }
1404 (Some(lbtc_address), _) => {
1405 let fees_sat = self
1408 .estimate_onchain_tx_or_drain_tx_fee(
1409 invoice_amount_sat,
1410 &lbtc_address,
1411 &asset_id,
1412 )
1413 .await?;
1414 (invoice_amount_sat, Some(fees_sat))
1415 }
1416 (None, _) => {
1417 let boltz_fees_total = lbtc_pair.fees.total(invoice_amount_sat);
1419 let user_lockup_amount_sat = invoice_amount_sat + boltz_fees_total;
1420 let lockup_fees_sat = self
1421 .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
1422 .await?;
1423 let fees_sat = boltz_fees_total + lockup_fees_sat;
1424 (invoice_amount_sat, Some(fees_sat))
1425 }
1426 };
1427
1428 payment_destination = SendDestination::Bolt11 {
1429 invoice,
1430 bip353_address: None,
1431 };
1432 }
1433 Ok(InputType::Bolt12Offer {
1434 offer,
1435 bip353_address,
1436 }) => {
1437 asset_id = self.config.lbtc_asset_id();
1438 estimated_asset_fees = None;
1439 (receiver_amount_sat, fees_sat) = match req.amount {
1440 Some(PayAmount::Drain) => {
1441 ensure_sdk!(
1442 get_info_res.wallet_info.pending_receive_sat == 0
1443 && get_info_res.wallet_info.pending_send_sat == 0,
1444 PaymentError::Generic {
1445 err: "Cannot drain while there are pending payments".to_string(),
1446 }
1447 );
1448 let lbtc_pair = self
1449 .swapper
1450 .get_submarine_pairs()
1451 .await?
1452 .ok_or(PaymentError::PairsNotFound)?;
1453 let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
1454 let drain_amount_sat =
1455 get_info_res.wallet_info.balance_sat - drain_fees_sat;
1456 let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
1458 let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
1459 let receiver_amount_sat =
1460 utils::increment_receiver_amount_up_to_drain_amount(
1461 dummy_amount_sat,
1462 &lbtc_pair,
1463 drain_amount_sat,
1464 );
1465 lbtc_pair.limits.within(receiver_amount_sat)?;
1466 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1468 ensure_sdk!(
1469 receiver_amount_sat + boltz_fees_total == drain_amount_sat,
1470 PaymentError::Generic {
1471 err: "Cannot drain without leaving a remainder".to_string(),
1472 }
1473 );
1474 let fees_sat = Some(boltz_fees_total + drain_fees_sat);
1475 info!("Drain amount: {receiver_amount_sat} sat");
1476 Ok((receiver_amount_sat, fees_sat))
1477 }
1478 Some(PayAmount::Bitcoin {
1479 receiver_amount_sat,
1480 }) => {
1481 let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
1482 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1483 let lockup_fees_sat = self
1484 .estimate_lockup_tx_or_drain_tx_fee(
1485 receiver_amount_sat + boltz_fees_total,
1486 )
1487 .await?;
1488 let fees_sat = Some(boltz_fees_total + lockup_fees_sat);
1489 Ok((receiver_amount_sat, fees_sat))
1490 }
1491 _ => Err(PaymentError::amount_missing(
1492 "Expected PayAmount of type Receiver when processing a Bolt12 offer",
1493 )),
1494 }?;
1495 if let Some(Amount::Bitcoin { amount_msat }) = &offer.min_amount {
1496 ensure_sdk!(
1497 receiver_amount_sat >= amount_msat / 1_000,
1498 PaymentError::invalid_invoice(
1499 "Invalid receiver amount: below offer minimum"
1500 )
1501 );
1502 }
1503
1504 payment_destination = SendDestination::Bolt12 {
1505 offer,
1506 receiver_amount_sat,
1507 bip353_address,
1508 };
1509 }
1510 _ => {
1511 return Err(PaymentError::generic("Destination is not valid"));
1512 }
1513 };
1514
1515 get_info_res.wallet_info.validate_sufficient_funds(
1516 self.config.network,
1517 receiver_amount_sat,
1518 fees_sat,
1519 &asset_id,
1520 )?;
1521
1522 Ok(PrepareSendResponse {
1523 destination: payment_destination,
1524 fees_sat,
1525 estimated_asset_fees,
1526 amount: req.amount.clone(),
1527 })
1528 }
1529
1530 fn ensure_send_is_not_self_transfer(&self, invoice: &str) -> Result<(), PaymentError> {
1531 match self.persister.fetch_receive_swap_by_invoice(invoice)? {
1532 None => Ok(()),
1533 Some(_) => Err(PaymentError::SelfTransferNotSupported),
1534 }
1535 }
1536
1537 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_payment_details(&None, 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_payment_details(&req.payer_note, bip353_address, &mut response)?;
1627 Ok(response)
1628 }
1629 SendDestination::Bolt12 {
1630 offer,
1631 receiver_amount_sat,
1632 bip353_address,
1633 } => {
1634 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1635 let bolt12_info = self
1636 .swapper
1637 .get_bolt12_info(GetBolt12FetchRequest {
1638 offer: offer.offer.clone(),
1639 amount: *receiver_amount_sat,
1640 note: req.payer_note.clone(),
1641 })
1642 .await?;
1643 let mut response = self
1644 .pay_bolt12_invoice(
1645 offer,
1646 *receiver_amount_sat,
1647 bolt12_info,
1648 fees_sat,
1649 is_drain,
1650 )
1651 .await?;
1652 self.insert_payment_details(&req.payer_note, bip353_address, &mut response)?;
1653 Ok(response)
1654 }
1655 }
1656 }
1657
1658 fn insert_payment_details(
1659 &self,
1660 payer_note: &Option<String>,
1661 bip353_address: &Option<String>,
1662 response: &mut SendPaymentResponse,
1663 ) -> Result<()> {
1664 if payer_note.is_some() || 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 bip353_address: bip353_address.clone(),
1673 payer_note: payer_note.clone(),
1674 ..Default::default()
1675 })?;
1676 if let Some(payment) = self.persister.get_payment(tx_id)? {
1678 response.payment = payment;
1679 }
1680 }
1681 }
1682 Ok(())
1683 }
1684
1685 async fn pay_bolt11_invoice(
1686 &self,
1687 invoice: &str,
1688 fees_sat: u64,
1689 is_drain: bool,
1690 ) -> Result<SendPaymentResponse, PaymentError> {
1691 self.ensure_send_is_not_self_transfer(invoice)?;
1692 let bolt11_invoice = self.validate_bolt11_invoice(invoice)?;
1693
1694 let amount_sat = bolt11_invoice
1695 .amount_milli_satoshis()
1696 .map(|msat| msat / 1_000)
1697 .ok_or(PaymentError::AmountMissing {
1698 err: "Invoice amount is missing".to_string(),
1699 })?;
1700 let payer_amount_sat = amount_sat + fees_sat;
1701 let get_info_response = self.get_info().await?;
1702 ensure_sdk!(
1703 payer_amount_sat <= get_info_response.wallet_info.balance_sat,
1704 PaymentError::InsufficientFunds
1705 );
1706
1707 let description = match bolt11_invoice.description() {
1708 Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
1709 Bolt11InvoiceDescription::Hash(_) => None,
1710 };
1711
1712 match self.swapper.check_for_mrh(invoice).await? {
1713 Some((address, _)) => {
1715 info!("Found MRH for L-BTC address {address}, invoice amount_sat {amount_sat}");
1716 let (amount_sat, fees_sat) = if is_drain {
1717 let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
1718 let drain_amount_sat =
1719 get_info_response.wallet_info.balance_sat - drain_fees_sat;
1720 info!("Drain amount: {drain_amount_sat} sat");
1721 (drain_amount_sat, drain_fees_sat)
1722 } else {
1723 (amount_sat, fees_sat)
1724 };
1725
1726 self.pay_liquid(
1727 LiquidAddressData {
1728 address,
1729 network: self.config.network.into(),
1730 asset_id: None,
1731 amount: None,
1732 amount_sat: None,
1733 label: None,
1734 message: None,
1735 },
1736 amount_sat,
1737 fees_sat,
1738 false,
1739 )
1740 .await
1741 }
1742
1743 None => {
1745 self.send_payment_via_swap(SendPaymentViaSwapRequest {
1746 invoice: invoice.to_string(),
1747 bolt12_offer: None,
1748 payment_hash: bolt11_invoice.payment_hash().to_string(),
1749 description,
1750 receiver_amount_sat: amount_sat,
1751 fees_sat,
1752 })
1753 .await
1754 }
1755 }
1756 }
1757
1758 async fn pay_bolt12_invoice(
1759 &self,
1760 offer: &LNOffer,
1761 user_specified_receiver_amount_sat: u64,
1762 bolt12_info: GetBolt12FetchResponse,
1763 fees_sat: u64,
1764 is_drain: bool,
1765 ) -> Result<SendPaymentResponse, PaymentError> {
1766 let invoice = self.validate_bolt12_invoice(
1767 offer,
1768 user_specified_receiver_amount_sat,
1769 &bolt12_info.invoice,
1770 )?;
1771
1772 let receiver_amount_sat = invoice.amount_msats() / 1_000;
1773 let payer_amount_sat = receiver_amount_sat + fees_sat;
1774 let get_info_response = self.get_info().await?;
1775 ensure_sdk!(
1776 payer_amount_sat <= get_info_response.wallet_info.balance_sat,
1777 PaymentError::InsufficientFunds
1778 );
1779
1780 match bolt12_info.magic_routing_hint {
1781 Some(MagicRoutingHint { bip21, signature }) => {
1783 info!(
1784 "Found MRH for L-BTC address {bip21}, invoice amount_sat {receiver_amount_sat}"
1785 );
1786 let signing_pubkey = invoice.signing_pubkey().to_string();
1787 let (_, address, _, _) = verify_mrh_signature(&bip21, &signing_pubkey, &signature)?;
1788 let (receiver_amount_sat, fees_sat) = if is_drain {
1789 let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
1790 let drain_amount_sat =
1791 get_info_response.wallet_info.balance_sat - drain_fees_sat;
1792 info!("Drain amount: {drain_amount_sat} sat");
1793 (drain_amount_sat, drain_fees_sat)
1794 } else {
1795 (receiver_amount_sat, fees_sat)
1796 };
1797
1798 self.pay_liquid(
1799 LiquidAddressData {
1800 address,
1801 network: self.config.network.into(),
1802 asset_id: None,
1803 amount: None,
1804 amount_sat: None,
1805 label: None,
1806 message: None,
1807 },
1808 receiver_amount_sat,
1809 fees_sat,
1810 false,
1811 )
1812 .await
1813 }
1814
1815 None => {
1817 self.send_payment_via_swap(SendPaymentViaSwapRequest {
1818 invoice: bolt12_info.invoice,
1819 bolt12_offer: Some(offer.offer.clone()),
1820 payment_hash: invoice.payment_hash().to_string(),
1821 description: invoice.description().map(|desc| desc.to_string()),
1822 receiver_amount_sat,
1823 fees_sat,
1824 })
1825 .await
1826 }
1827 }
1828 }
1829
1830 async fn pay_liquid(
1832 &self,
1833 address_data: LiquidAddressData,
1834 receiver_amount_sat: u64,
1835 fees_sat: u64,
1836 skip_already_paid_check: bool,
1837 ) -> Result<SendPaymentResponse, PaymentError> {
1838 let destination = address_data
1839 .to_uri()
1840 .unwrap_or(address_data.address.clone());
1841 let asset_id = address_data.asset_id.unwrap_or(self.config.lbtc_asset_id());
1842 let payments = self.persister.get_payments(&ListPaymentsRequest {
1843 details: Some(ListPaymentDetails::Liquid {
1844 asset_id: Some(asset_id.clone()),
1845 destination: Some(destination.clone()),
1846 }),
1847 ..Default::default()
1848 })?;
1849 ensure_sdk!(
1850 skip_already_paid_check || payments.is_empty(),
1851 PaymentError::AlreadyPaid
1852 );
1853
1854 let tx = self
1855 .onchain_wallet
1856 .build_tx_or_drain_tx(
1857 Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
1858 &address_data.address,
1859 &asset_id,
1860 receiver_amount_sat,
1861 )
1862 .await?;
1863 let tx_id = tx.txid().to_string();
1864 let tx_fees_sat = tx.all_fees().values().sum::<u64>();
1865 ensure_sdk!(tx_fees_sat <= fees_sat, PaymentError::InvalidOrExpiredFees);
1866
1867 info!(
1868 "Built onchain Liquid tx with receiver_amount_sat = {receiver_amount_sat}, fees_sat = {fees_sat} and txid = {tx_id}"
1869 );
1870
1871 let tx_id = self.liquid_chain_service.broadcast(&tx).await?.to_string();
1872
1873 let tx_data = PaymentTxData {
1876 tx_id: tx_id.clone(),
1877 timestamp: Some(utils::now()),
1878 amount: receiver_amount_sat,
1879 fees_sat,
1880 payment_type: PaymentType::Send,
1881 is_confirmed: false,
1882 unblinding_data: None,
1883 asset_id: asset_id.clone(),
1884 };
1885
1886 let description = address_data.message;
1887
1888 self.persister.insert_or_update_payment(
1889 tx_data.clone(),
1890 Some(PaymentTxDetails {
1891 tx_id: tx_id.clone(),
1892 destination: destination.clone(),
1893 description: description.clone(),
1894 ..Default::default()
1895 }),
1896 false,
1897 )?;
1898 self.emit_payment_updated(Some(tx_id)).await?; let asset_info = self
1901 .persister
1902 .get_asset_metadata(&asset_id)?
1903 .map(|ref am| AssetInfo {
1904 name: am.name.clone(),
1905 ticker: am.ticker.clone(),
1906 amount: am.amount_from_sat(receiver_amount_sat),
1907 fees: None,
1908 });
1909 let payment_details = PaymentDetails::Liquid {
1910 asset_id,
1911 destination,
1912 description: description.unwrap_or("Liquid transfer".to_string()),
1913 asset_info,
1914 lnurl_info: None,
1915 bip353_address: None,
1916 payer_note: 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 payer_note: None,
1998 };
1999
2000 Ok(SendPaymentResponse {
2001 payment: Payment::from_tx_data(tx_data, None, payment_details),
2002 })
2003 }
2004
2005 async fn send_payment_via_swap(
2009 &self,
2010 req: SendPaymentViaSwapRequest,
2011 ) -> Result<SendPaymentResponse, PaymentError> {
2012 let SendPaymentViaSwapRequest {
2013 invoice,
2014 bolt12_offer,
2015 payment_hash,
2016 description,
2017 receiver_amount_sat,
2018 fees_sat,
2019 } = req;
2020 let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
2021 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
2022 let user_lockup_amount_sat = receiver_amount_sat + boltz_fees_total;
2023 let lockup_tx_fees_sat = self
2024 .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
2025 .await?;
2026 ensure_sdk!(
2027 fees_sat == boltz_fees_total + lockup_tx_fees_sat,
2028 PaymentError::InvalidOrExpiredFees
2029 );
2030
2031 let swap = match self.persister.fetch_send_swap_by_invoice(&invoice)? {
2032 Some(swap) => match swap.state {
2033 Created => swap,
2034 TimedOut => {
2035 self.send_swap_handler.update_swap_info(
2036 &swap.id,
2037 PaymentState::Created,
2038 None,
2039 None,
2040 None,
2041 )?;
2042 swap
2043 }
2044 Pending => return Err(PaymentError::PaymentInProgress),
2045 Complete => return Err(PaymentError::AlreadyPaid),
2046 RefundPending | Refundable | Failed => {
2047 return Err(PaymentError::invalid_invoice(
2048 "Payment has already failed. Please try with another invoice",
2049 ))
2050 }
2051 WaitingFeeAcceptance => {
2052 return Err(PaymentError::Generic {
2053 err: "Send swap payment cannot be in state WaitingFeeAcceptance"
2054 .to_string(),
2055 })
2056 }
2057 },
2058 None => {
2059 let keypair = utils::generate_keypair();
2060 let refund_public_key = boltz_client::PublicKey {
2061 compressed: true,
2062 inner: keypair.public_key(),
2063 };
2064 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2065 url,
2066 hash_swap_id: Some(true),
2067 status: Some(vec![
2068 SubSwapStates::InvoiceFailedToPay,
2069 SubSwapStates::SwapExpired,
2070 SubSwapStates::TransactionClaimPending,
2071 SubSwapStates::TransactionLockupFailed,
2072 ]),
2073 });
2074 let create_response = self
2075 .swapper
2076 .create_send_swap(CreateSubmarineRequest {
2077 from: "L-BTC".to_string(),
2078 to: "BTC".to_string(),
2079 invoice: invoice.to_string(),
2080 refund_public_key,
2081 pair_hash: Some(lbtc_pair.hash.clone()),
2082 referral_id: None,
2083 webhook,
2084 })
2085 .await?;
2086
2087 let swap_id = &create_response.id;
2088 let create_response_json =
2089 SendSwap::from_boltz_struct_to_json(&create_response, swap_id)?;
2090 let destination_pubkey =
2091 utils::get_invoice_destination_pubkey(&invoice, bolt12_offer.is_some())?;
2092
2093 let payer_amount_sat = fees_sat + receiver_amount_sat;
2094 let swap = SendSwap {
2095 id: swap_id.to_string(),
2096 invoice: invoice.to_string(),
2097 bolt12_offer,
2098 payment_hash: Some(payment_hash.to_string()),
2099 destination_pubkey: Some(destination_pubkey),
2100 timeout_block_height: create_response.timeout_block_height,
2101 description,
2102 preimage: None,
2103 payer_amount_sat,
2104 receiver_amount_sat,
2105 pair_fees_json: serde_json::to_string(&lbtc_pair).map_err(|e| {
2106 PaymentError::generic(format!("Failed to serialize SubmarinePair: {e:?}"))
2107 })?,
2108 create_response_json,
2109 lockup_tx_id: None,
2110 refund_address: None,
2111 refund_tx_id: None,
2112 created_at: utils::now(),
2113 state: PaymentState::Created,
2114 refund_private_key: keypair.display_secret().to_string(),
2115 metadata: Default::default(),
2116 };
2117 self.persister.insert_or_update_send_swap(&swap)?;
2118 swap
2119 }
2120 };
2121 self.status_stream.track_swap_id(&swap.id)?;
2122
2123 let create_response = swap.get_boltz_create_response()?;
2124 self.send_swap_handler
2125 .try_lockup(&swap, &create_response)
2126 .await?;
2127
2128 self.wait_for_payment_with_timeout(Swap::Send(swap), create_response.accept_zero_conf)
2129 .await
2130 .map(|payment| SendPaymentResponse { payment })
2131 }
2132
2133 pub async fn fetch_lightning_limits(
2135 &self,
2136 ) -> Result<LightningPaymentLimitsResponse, PaymentError> {
2137 self.ensure_is_started().await?;
2138
2139 let submarine_pair = self
2140 .swapper
2141 .get_submarine_pairs()
2142 .await?
2143 .ok_or(PaymentError::PairsNotFound)?;
2144 let send_limits = submarine_pair.limits;
2145
2146 let reverse_pair = self
2147 .swapper
2148 .get_reverse_swap_pairs()
2149 .await?
2150 .ok_or(PaymentError::PairsNotFound)?;
2151 let receive_limits = reverse_pair.limits;
2152
2153 Ok(LightningPaymentLimitsResponse {
2154 send: Limits {
2155 min_sat: send_limits.minimal_batched.unwrap_or(send_limits.minimal),
2156 max_sat: send_limits.maximal,
2157 max_zero_conf_sat: send_limits.maximal_zero_conf,
2158 },
2159 receive: Limits {
2160 min_sat: receive_limits.minimal,
2161 max_sat: receive_limits.maximal,
2162 max_zero_conf_sat: self.config.zero_conf_max_amount_sat(),
2163 },
2164 })
2165 }
2166
2167 pub async fn fetch_onchain_limits(&self) -> Result<OnchainPaymentLimitsResponse, PaymentError> {
2169 self.ensure_is_started().await?;
2170
2171 let (pair_outgoing, pair_incoming) = self.swapper.get_chain_pairs().await?;
2172 let send_limits = pair_outgoing
2173 .ok_or(PaymentError::PairsNotFound)
2174 .map(|pair| pair.limits)?;
2175 let receive_limits = pair_incoming
2176 .ok_or(PaymentError::PairsNotFound)
2177 .map(|pair| pair.limits)?;
2178
2179 Ok(OnchainPaymentLimitsResponse {
2180 send: Limits {
2181 min_sat: send_limits.minimal,
2182 max_sat: send_limits.maximal,
2183 max_zero_conf_sat: send_limits.maximal_zero_conf,
2184 },
2185 receive: Limits {
2186 min_sat: receive_limits.minimal,
2187 max_sat: receive_limits.maximal,
2188 max_zero_conf_sat: receive_limits.maximal_zero_conf,
2189 },
2190 })
2191 }
2192
2193 pub async fn prepare_pay_onchain(
2202 &self,
2203 req: &PreparePayOnchainRequest,
2204 ) -> Result<PreparePayOnchainResponse, PaymentError> {
2205 self.ensure_is_started().await?;
2206
2207 let get_info_res = self.get_info().await?;
2208 let pair = self.get_chain_pair(Direction::Outgoing).await?;
2209 let claim_fees_sat = match req.fee_rate_sat_per_vbyte {
2210 Some(sat_per_vbyte) => ESTIMATED_BTC_CLAIM_TX_VSIZE * sat_per_vbyte as u64,
2211 None => pair.clone().fees.claim_estimate(),
2212 };
2213 let server_fees_sat = pair.fees.server();
2214
2215 info!("Preparing for onchain payment of kind: {:?}", req.amount);
2216 let (payer_amount_sat, receiver_amount_sat, total_fees_sat) = match req.amount {
2217 PayAmount::Bitcoin {
2218 receiver_amount_sat: amount_sat,
2219 } => {
2220 let receiver_amount_sat = amount_sat;
2221
2222 let user_lockup_amount_sat_without_service_fee =
2223 receiver_amount_sat + claim_fees_sat + server_fees_sat;
2224
2225 let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64
2228 * 100.0
2229 / (100.0 - pair.fees.percentage))
2230 .ceil() as u64;
2231 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2232
2233 let lockup_fees_sat = self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?;
2234
2235 let boltz_fees_sat =
2236 user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2237 let total_fees_sat =
2238 boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2239 let payer_amount_sat = receiver_amount_sat + total_fees_sat;
2240
2241 (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2242 }
2243 PayAmount::Drain => {
2244 ensure_sdk!(
2245 get_info_res.wallet_info.pending_receive_sat == 0
2246 && get_info_res.wallet_info.pending_send_sat == 0,
2247 PaymentError::Generic {
2248 err: "Cannot drain while there are pending payments".to_string(),
2249 }
2250 );
2251 let payer_amount_sat = get_info_res.wallet_info.balance_sat;
2252 let lockup_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
2253
2254 let user_lockup_amount_sat = payer_amount_sat - lockup_fees_sat;
2255 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2256
2257 let boltz_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
2258 let total_fees_sat =
2259 boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2260 let receiver_amount_sat = payer_amount_sat - total_fees_sat;
2261
2262 (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2263 }
2264 PayAmount::Asset { .. } => {
2265 return Err(PaymentError::asset_error(
2266 "Cannot send an asset to a Bitcoin address",
2267 ))
2268 }
2269 };
2270
2271 let res = PreparePayOnchainResponse {
2272 receiver_amount_sat,
2273 claim_fees_sat,
2274 total_fees_sat,
2275 };
2276
2277 ensure_sdk!(
2278 payer_amount_sat <= get_info_res.wallet_info.balance_sat,
2279 PaymentError::InsufficientFunds
2280 );
2281
2282 info!("Prepared onchain payment: {res:?}");
2283 Ok(res)
2284 }
2285
2286 pub async fn pay_onchain(
2303 &self,
2304 req: &PayOnchainRequest,
2305 ) -> Result<SendPaymentResponse, PaymentError> {
2306 self.ensure_is_started().await?;
2307 info!("Paying onchain, request = {req:?}");
2308
2309 let claim_address = self.validate_bitcoin_address(&req.address).await?;
2310 let balance_sat = self.get_info().await?.wallet_info.balance_sat;
2311 let receiver_amount_sat = req.prepare_response.receiver_amount_sat;
2312 let pair = self.get_chain_pair(Direction::Outgoing).await?;
2313 let claim_fees_sat = req.prepare_response.claim_fees_sat;
2314 let server_fees_sat = pair.fees.server();
2315 let server_lockup_amount_sat = receiver_amount_sat + claim_fees_sat;
2316
2317 let user_lockup_amount_sat_without_service_fee =
2318 receiver_amount_sat + claim_fees_sat + server_fees_sat;
2319
2320 let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64 * 100.0
2323 / (100.0 - pair.fees.percentage))
2324 .ceil() as u64;
2325 let boltz_fee_sat = user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2326 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2327
2328 let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2329
2330 let lockup_fees_sat = match payer_amount_sat == balance_sat {
2331 true => self.estimate_drain_tx_fee(None, None).await?,
2332 false => self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?,
2333 };
2334
2335 ensure_sdk!(
2336 req.prepare_response.total_fees_sat
2337 == boltz_fee_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat,
2338 PaymentError::InvalidOrExpiredFees
2339 );
2340
2341 ensure_sdk!(
2342 payer_amount_sat <= balance_sat,
2343 PaymentError::InsufficientFunds
2344 );
2345
2346 let preimage = Preimage::new();
2347 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2348
2349 let claim_keypair = utils::generate_keypair();
2350 let claim_public_key = boltz_client::PublicKey {
2351 compressed: true,
2352 inner: claim_keypair.public_key(),
2353 };
2354 let refund_keypair = utils::generate_keypair();
2355 let refund_public_key = boltz_client::PublicKey {
2356 compressed: true,
2357 inner: refund_keypair.public_key(),
2358 };
2359 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2360 url,
2361 hash_swap_id: Some(true),
2362 status: Some(vec![
2363 ChainSwapStates::TransactionFailed,
2364 ChainSwapStates::TransactionLockupFailed,
2365 ChainSwapStates::TransactionServerConfirmed,
2366 ]),
2367 });
2368 let create_response = self
2369 .swapper
2370 .create_chain_swap(CreateChainRequest {
2371 from: "L-BTC".to_string(),
2372 to: "BTC".to_string(),
2373 preimage_hash: preimage.sha256,
2374 claim_public_key: Some(claim_public_key),
2375 refund_public_key: Some(refund_public_key),
2376 user_lock_amount: None,
2377 server_lock_amount: Some(server_lockup_amount_sat),
2378 pair_hash: Some(pair.hash.clone()),
2379 referral_id: None,
2380 webhook,
2381 })
2382 .await?;
2383
2384 let create_response_json =
2385 ChainSwap::from_boltz_struct_to_json(&create_response, &create_response.id)?;
2386 let swap_id = create_response.id;
2387
2388 let accept_zero_conf = server_lockup_amount_sat <= pair.limits.maximal_zero_conf;
2389 let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2390
2391 let swap = ChainSwap {
2392 id: swap_id.clone(),
2393 direction: Direction::Outgoing,
2394 claim_address: Some(claim_address),
2395 lockup_address: create_response.lockup_details.lockup_address,
2396 refund_address: None,
2397 timeout_block_height: create_response.lockup_details.timeout_block_height,
2398 preimage: preimage_str,
2399 description: Some("Bitcoin transfer".to_string()),
2400 payer_amount_sat,
2401 actual_payer_amount_sat: None,
2402 receiver_amount_sat,
2403 accepted_receiver_amount_sat: None,
2404 claim_fees_sat,
2405 pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
2406 PaymentError::generic(format!("Failed to serialize outgoing ChainPair: {e:?}"))
2407 })?,
2408 accept_zero_conf,
2409 create_response_json,
2410 claim_private_key: claim_keypair.display_secret().to_string(),
2411 refund_private_key: refund_keypair.display_secret().to_string(),
2412 server_lockup_tx_id: None,
2413 user_lockup_tx_id: None,
2414 claim_tx_id: None,
2415 refund_tx_id: None,
2416 created_at: utils::now(),
2417 state: PaymentState::Created,
2418 auto_accepted_fees: false,
2419 metadata: Default::default(),
2420 };
2421 self.persister.insert_or_update_chain_swap(&swap)?;
2422 self.status_stream.track_swap_id(&swap_id)?;
2423
2424 self.wait_for_payment_with_timeout(Swap::Chain(swap), accept_zero_conf)
2425 .await
2426 .map(|payment| SendPaymentResponse { payment })
2427 }
2428
2429 async fn wait_for_payment_with_timeout(
2430 &self,
2431 swap: Swap,
2432 accept_zero_conf: bool,
2433 ) -> Result<Payment, PaymentError> {
2434 let timeout_fut = tokio::time::sleep(Duration::from_secs(self.config.payment_timeout_sec));
2435 tokio::pin!(timeout_fut);
2436
2437 let expected_swap_id = swap.id();
2438 let mut events_stream = self.event_manager.subscribe();
2439 let mut maybe_payment: Option<Payment> = None;
2440
2441 loop {
2442 tokio::select! {
2443 _ = &mut timeout_fut => match maybe_payment {
2444 Some(payment) => return Ok(payment),
2445 None => {
2446 debug!("Timeout occurred without payment, set swap to timed out");
2447 let update_res = match swap {
2448 Swap::Send(_) => self.send_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None),
2449 Swap::Chain(_) => self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
2450 swap_id: expected_swap_id.clone(),
2451 to_state: TimedOut,
2452 ..Default::default()
2453 }),
2454 _ => Ok(())
2455 };
2456 return match update_res {
2457 Ok(_) => Err(PaymentError::PaymentTimeout),
2458 Err(_) => {
2459 self.persister.get_payment(&expected_swap_id).ok().flatten().ok_or(PaymentError::generic("Payment not found"))
2462 }
2463 }
2464 },
2465 },
2466 event = events_stream.recv() => match event {
2467 Ok(SdkEvent::PaymentPending { details: payment }) => {
2468 let maybe_payment_swap_id = payment.details.get_swap_id();
2469 if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2470 match accept_zero_conf {
2471 true => {
2472 debug!("Received Send Payment pending event with zero-conf accepted");
2473 return Ok(payment)
2474 }
2475 false => {
2476 debug!("Received Send Payment pending event, waiting for confirmation");
2477 maybe_payment = Some(payment);
2478 }
2479 }
2480 };
2481 },
2482 Ok(SdkEvent::PaymentSucceeded { details: payment }) => {
2483 let maybe_payment_swap_id = payment.details.get_swap_id();
2484 if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2485 debug!("Received Send Payment succeed event");
2486 return Ok(payment);
2487 }
2488 },
2489 Ok(event) => debug!("Unhandled event waiting for payment: {event:?}"),
2490 Err(e) => debug!("Received error waiting for payment: {e:?}"),
2491 }
2492 }
2493 }
2494 }
2495
2496 pub async fn prepare_receive_payment(
2506 &self,
2507 req: &PrepareReceiveRequest,
2508 ) -> Result<PrepareReceiveResponse, PaymentError> {
2509 self.ensure_is_started().await?;
2510
2511 match req.payment_method.clone() {
2512 #[allow(deprecated)]
2513 PaymentMethod::Bolt11Invoice | PaymentMethod::Lightning => {
2514 let payer_amount_sat = match req.amount {
2515 Some(ReceiveAmount::Asset { .. }) => {
2516 return Err(PaymentError::asset_error(
2517 "Cannot receive an asset for this payment method",
2518 ));
2519 }
2520 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
2521 None => {
2522 return Err(PaymentError::generic(
2523 "Bitcoin payer amount must be set for this payment method",
2524 ));
2525 }
2526 };
2527 let reverse_pair = self
2528 .swapper
2529 .get_reverse_swap_pairs()
2530 .await?
2531 .ok_or(PaymentError::PairsNotFound)?;
2532
2533 let fees_sat = reverse_pair.fees.total(payer_amount_sat);
2534
2535 reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
2536 PaymentError::AmountOutOfRange {
2537 min: reverse_pair.limits.minimal,
2538 max: reverse_pair.limits.maximal,
2539 }
2540 })?;
2541
2542 let min_payer_amount_sat = Some(reverse_pair.limits.minimal);
2543 let max_payer_amount_sat = Some(reverse_pair.limits.maximal);
2544 let swapper_feerate = Some(reverse_pair.fees.percentage);
2545
2546 debug!(
2547 "Preparing Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat"
2548 );
2549
2550 Ok(PrepareReceiveResponse {
2551 payment_method: req.payment_method.clone(),
2552 amount: req.amount.clone(),
2553 fees_sat,
2554 min_payer_amount_sat,
2555 max_payer_amount_sat,
2556 swapper_feerate,
2557 })
2558 }
2559 PaymentMethod::Bolt12Offer => {
2560 if req.amount.is_some() {
2561 return Err(PaymentError::generic(
2562 "Amount cannot be set for this payment method",
2563 ));
2564 }
2565
2566 let reverse_pair = self
2567 .swapper
2568 .get_reverse_swap_pairs()
2569 .await?
2570 .ok_or(PaymentError::PairsNotFound)?;
2571
2572 let fees_sat = reverse_pair.fees.total(0);
2573 debug!("Preparing Bolt12Offer Receive Swap with: min fees_sat {fees_sat}");
2574
2575 Ok(PrepareReceiveResponse {
2576 payment_method: req.payment_method.clone(),
2577 amount: req.amount.clone(),
2578 fees_sat,
2579 min_payer_amount_sat: Some(reverse_pair.limits.minimal),
2580 max_payer_amount_sat: Some(reverse_pair.limits.maximal),
2581 swapper_feerate: Some(reverse_pair.fees.percentage),
2582 })
2583 }
2584 PaymentMethod::BitcoinAddress => {
2585 let payer_amount_sat = match req.amount {
2586 Some(ReceiveAmount::Asset { .. }) => {
2587 return Err(PaymentError::asset_error(
2588 "Asset cannot be received for this payment method",
2589 ));
2590 }
2591 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
2592 None => None,
2593 };
2594 let pair = self
2595 .get_and_validate_chain_pair(Direction::Incoming, payer_amount_sat)
2596 .await?;
2597 let claim_fees_sat = pair.fees.claim_estimate();
2598 let server_fees_sat = pair.fees.server();
2599 let service_fees_sat = payer_amount_sat
2600 .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
2601 .unwrap_or_default();
2602
2603 let fees_sat = service_fees_sat + claim_fees_sat + server_fees_sat;
2604 debug!("Preparing Chain Receive Swap with: payer_amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}");
2605
2606 Ok(PrepareReceiveResponse {
2607 payment_method: req.payment_method.clone(),
2608 amount: req.amount.clone(),
2609 fees_sat,
2610 min_payer_amount_sat: Some(pair.limits.minimal),
2611 max_payer_amount_sat: Some(pair.limits.maximal),
2612 swapper_feerate: Some(pair.fees.percentage),
2613 })
2614 }
2615 PaymentMethod::LiquidAddress => {
2616 let (asset_id, payer_amount, payer_amount_sat) = match req.amount.clone() {
2617 Some(ReceiveAmount::Asset {
2618 payer_amount,
2619 asset_id,
2620 }) => (asset_id, payer_amount, None),
2621 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
2622 (self.config.lbtc_asset_id(), None, Some(payer_amount_sat))
2623 }
2624 None => (self.config.lbtc_asset_id(), None, None),
2625 };
2626
2627 debug!("Preparing Liquid Receive with: asset_id {asset_id}, amount {payer_amount:?}, amount_sat {payer_amount_sat:?}");
2628
2629 Ok(PrepareReceiveResponse {
2630 payment_method: req.payment_method.clone(),
2631 amount: req.amount.clone(),
2632 fees_sat: 0,
2633 min_payer_amount_sat: None,
2634 max_payer_amount_sat: None,
2635 swapper_feerate: None,
2636 })
2637 }
2638 }
2639 }
2640
2641 pub async fn receive_payment(
2661 &self,
2662 req: &ReceivePaymentRequest,
2663 ) -> Result<ReceivePaymentResponse, PaymentError> {
2664 self.ensure_is_started().await?;
2665
2666 let PrepareReceiveResponse {
2667 payment_method,
2668 amount,
2669 fees_sat,
2670 ..
2671 } = req.prepare_response.clone();
2672
2673 match payment_method {
2674 #[allow(deprecated)]
2675 PaymentMethod::Bolt11Invoice | PaymentMethod::Lightning => {
2676 let amount_sat = match amount.clone() {
2677 Some(ReceiveAmount::Asset { .. }) => {
2678 return Err(PaymentError::asset_error(
2679 "Asset cannot be received for this payment method",
2680 ));
2681 }
2682 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
2683 None => {
2684 return Err(PaymentError::generic(
2685 "Bitcoin payer amount must be set for this payment method",
2686 ));
2687 }
2688 };
2689 let (description, description_hash) = match (
2690 req.description.clone(),
2691 req.use_description_hash.unwrap_or_default(),
2692 ) {
2693 (Some(description), true) => (
2694 None,
2695 Some(sha256::Hash::hash(description.as_bytes()).to_hex()),
2696 ),
2697 (_, false) => (req.description.clone(), None),
2698 _ => {
2699 return Err(PaymentError::InvalidDescription {
2700 err: "Missing payment description to hash".to_string(),
2701 })
2702 }
2703 };
2704 self.create_bolt11_receive_swap(
2705 amount_sat,
2706 fees_sat,
2707 description,
2708 description_hash,
2709 req.payer_note.clone(),
2710 )
2711 .await
2712 }
2713 PaymentMethod::Bolt12Offer => {
2714 let description = req.description.clone().unwrap_or("".to_string());
2715 match self
2716 .persister
2717 .fetch_bolt12_offer_by_description(&description)?
2718 {
2719 Some(bolt12_offer) => Ok(ReceivePaymentResponse {
2720 destination: bolt12_offer.id,
2721 }),
2722 None => self.create_bolt12_offer(description).await,
2723 }
2724 }
2725 PaymentMethod::BitcoinAddress => {
2726 let amount_sat = match amount.clone() {
2727 Some(ReceiveAmount::Asset { .. }) => {
2728 return Err(PaymentError::asset_error(
2729 "Asset cannot be received for this payment method",
2730 ));
2731 }
2732 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
2733 None => None,
2734 };
2735 self.receive_onchain(amount_sat, fees_sat).await
2736 }
2737 PaymentMethod::LiquidAddress => {
2738 let lbtc_asset_id = self.config.lbtc_asset_id();
2739 let (asset_id, amount, amount_sat) = match amount.clone() {
2740 Some(ReceiveAmount::Asset {
2741 asset_id,
2742 payer_amount,
2743 }) => (asset_id, payer_amount, None),
2744 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
2745 (lbtc_asset_id.clone(), None, Some(payer_amount_sat))
2746 }
2747 None => (lbtc_asset_id.clone(), None, None),
2748 };
2749
2750 let address = self.onchain_wallet.next_unused_address().await?.to_string();
2751 let receive_destination =
2752 if asset_id.ne(&lbtc_asset_id) || amount.is_some() || amount_sat.is_some() {
2753 LiquidAddressData {
2754 address: address.to_string(),
2755 network: self.config.network.into(),
2756 amount,
2757 amount_sat,
2758 asset_id: Some(asset_id),
2759 label: None,
2760 message: req.description.clone(),
2761 }
2762 .to_uri()
2763 .map_err(|e| PaymentError::Generic {
2764 err: format!("Could not build BIP21 URI: {e:?}"),
2765 })?
2766 } else {
2767 address
2768 };
2769
2770 Ok(ReceivePaymentResponse {
2771 destination: receive_destination,
2772 })
2773 }
2774 }
2775 }
2776
2777 async fn create_bolt11_receive_swap(
2778 &self,
2779 payer_amount_sat: u64,
2780 fees_sat: u64,
2781 description: Option<String>,
2782 description_hash: Option<String>,
2783 payer_note: Option<String>,
2784 ) -> Result<ReceivePaymentResponse, PaymentError> {
2785 let reverse_pair = self
2786 .swapper
2787 .get_reverse_swap_pairs()
2788 .await?
2789 .ok_or(PaymentError::PairsNotFound)?;
2790 let new_fees_sat = reverse_pair.fees.total(payer_amount_sat);
2791 ensure_sdk!(fees_sat == new_fees_sat, PaymentError::InvalidOrExpiredFees);
2792
2793 debug!("Creating BOLT11 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
2794
2795 let keypair = utils::generate_keypair();
2796
2797 let preimage = Preimage::new();
2798 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2799 let preimage_hash = preimage.sha256.to_string();
2800
2801 let mrh_addr = self.onchain_wallet.next_unused_address().await?;
2803 let mrh_addr_str = mrh_addr.to_string();
2805 let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
2806
2807 let receiver_amount_sat = payer_amount_sat - fees_sat;
2808 let webhook_claim_status =
2809 match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
2810 true => RevSwapStates::TransactionConfirmed,
2811 false => RevSwapStates::TransactionMempool,
2812 };
2813 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2814 url,
2815 hash_swap_id: Some(true),
2816 status: Some(vec![webhook_claim_status]),
2817 });
2818
2819 let v2_req = CreateReverseRequest {
2820 from: "BTC".to_string(),
2821 to: "L-BTC".to_string(),
2822 invoice: None,
2823 invoice_amount: Some(payer_amount_sat),
2824 preimage_hash: Some(preimage.sha256),
2825 claim_public_key: keypair.public_key().into(),
2826 description,
2827 description_hash,
2828 address: Some(mrh_addr_str.clone()),
2829 address_signature: Some(mrh_addr_hash_sig.to_hex()),
2830 referral_id: None,
2831 webhook,
2832 };
2833 let create_response = self.swapper.create_receive_swap(v2_req).await?;
2834 let invoice_str = create_response
2835 .invoice
2836 .clone()
2837 .ok_or(PaymentError::receive_error("Invoice not found"))?;
2838
2839 self.persister.insert_or_update_reserved_address(
2841 &mrh_addr_str,
2842 create_response.timeout_block_height,
2843 )?;
2844
2845 let (bip21_lbtc_address, _bip21_amount_btc) = self
2847 .swapper
2848 .check_for_mrh(&invoice_str)
2849 .await?
2850 .ok_or(PaymentError::receive_error("Invoice has no MRH"))?;
2851 ensure_sdk!(
2852 bip21_lbtc_address == mrh_addr_str,
2853 PaymentError::receive_error("Invoice has incorrect address in MRH")
2854 );
2855
2856 let swap_id = create_response.id.clone();
2857 let invoice = Bolt11Invoice::from_str(&invoice_str)
2858 .map_err(|err| PaymentError::invalid_invoice(err.to_string()))?;
2859 let payer_amount_sat =
2860 invoice
2861 .amount_milli_satoshis()
2862 .ok_or(PaymentError::invalid_invoice(
2863 "Invoice does not contain an amount",
2864 ))?
2865 / 1000;
2866 let destination_pubkey = invoice_pubkey(&invoice);
2867
2868 ensure_sdk!(
2871 invoice.payment_hash().to_string() == preimage_hash,
2872 PaymentError::invalid_invoice("Invalid preimage returned by swapper")
2873 );
2874
2875 let create_response_json = ReceiveSwap::from_boltz_struct_to_json(
2876 &create_response,
2877 &swap_id,
2878 Some(&invoice.to_string()),
2879 )?;
2880 let invoice_description = match invoice.description() {
2881 Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
2882 Bolt11InvoiceDescription::Hash(_) => None,
2883 };
2884
2885 self.persister
2886 .insert_or_update_receive_swap(&ReceiveSwap {
2887 id: swap_id.clone(),
2888 preimage: preimage_str,
2889 create_response_json,
2890 claim_private_key: keypair.display_secret().to_string(),
2891 invoice: invoice.to_string(),
2892 bolt12_offer: None,
2893 payment_hash: Some(preimage_hash),
2894 destination_pubkey: Some(destination_pubkey),
2895 timeout_block_height: create_response.timeout_block_height,
2896 description: invoice_description,
2897 payer_note,
2898 payer_amount_sat,
2899 receiver_amount_sat,
2900 pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
2901 PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
2902 })?,
2903 claim_fees_sat: reverse_pair.fees.claim_estimate(),
2904 lockup_tx_id: None,
2905 claim_address: None,
2906 claim_tx_id: None,
2907 mrh_address: mrh_addr_str,
2908 mrh_tx_id: None,
2909 created_at: utils::now(),
2910 state: PaymentState::Created,
2911 metadata: Default::default(),
2912 })
2913 .map_err(|_| PaymentError::PersistError)?;
2914 self.status_stream.track_swap_id(&swap_id)?;
2915
2916 Ok(ReceivePaymentResponse {
2917 destination: invoice.to_string(),
2918 })
2919 }
2920
2921 pub async fn create_bolt12_invoice(
2934 &self,
2935 req: &CreateBolt12InvoiceRequest,
2936 ) -> Result<CreateBolt12InvoiceResponse, PaymentError> {
2937 debug!("Started create BOLT12 invoice");
2938 let bolt12_offer =
2939 self.persister
2940 .fetch_bolt12_offer_by_id(&req.offer)?
2941 .ok_or(PaymentError::generic(format!(
2942 "Bolt12 offer not found: {}",
2943 req.offer
2944 )))?;
2945 let offer = Offer::try_from(bolt12_offer.clone())?;
2947 let cln_node_public_key = offer
2948 .paths()
2949 .iter()
2950 .find_map(|path| match path.introduction_node().clone() {
2951 IntroductionNode::NodeId(node_id) => Some(node_id),
2952 IntroductionNode::DirectedShortChannelId(_, _) => None,
2953 })
2954 .ok_or(PaymentError::generic(format!(
2955 "No BTC CLN node found: {}",
2956 req.offer
2957 )))?;
2958 let invoice_request = utils::bolt12::decode_invoice_request(&req.invoice_request)?;
2959 let payer_amount_sat = invoice_request
2960 .amount_msats()
2961 .map(|msats| msats / 1_000)
2962 .ok_or(PaymentError::amount_missing(
2963 "Invoice request must contain an amount",
2964 ))?;
2965 let (params, maybe_reverse_pair) = tokio::try_join!(
2967 self.swapper.get_bolt12_params(),
2968 self.swapper.get_reverse_swap_pairs()
2969 )?;
2970 let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
2971 reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
2972 PaymentError::AmountOutOfRange {
2973 min: reverse_pair.limits.minimal,
2974 max: reverse_pair.limits.maximal,
2975 }
2976 })?;
2977 let fees_sat = reverse_pair.fees.total(payer_amount_sat);
2978 debug!("Creating BOLT12 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
2979
2980 let secp = Secp256k1::new();
2981 let keypair = bolt12_offer.get_keypair()?;
2982 let preimage = Preimage::new();
2983 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2984 let preimage_hash = preimage.sha256.to_byte_array();
2985
2986 let mrh_addr = self.onchain_wallet.next_unused_address().await?;
2988 let mrh_addr_str = mrh_addr.to_string();
2990 let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
2991
2992 let entropy_source = RandomBytes::new(utils::generate_entropy());
2993 let nonce = Nonce::from_entropy_source(&entropy_source);
2994 let payer_note = invoice_request.payer_note().map(|s| s.to_string());
2995 let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
2996 offer_id: Offer::try_from(bolt12_offer)?.id(),
2997 invoice_request: InvoiceRequestFields {
2998 payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
2999 quantity: invoice_request.quantity(),
3000 payer_note_truncated: payer_note.clone().map(UntrustedString),
3001 human_readable_name: invoice_request.offer_from_hrn().clone(),
3002 },
3003 });
3004 let expanded_key = ExpandedKey::new(keypair.secret_key().secret_bytes());
3005 let payee_tlvs = UnauthenticatedReceiveTlvs {
3006 payment_secret: PaymentSecret(utils::generate_entropy()),
3007 payment_constraints: PaymentConstraints {
3008 max_cltv_expiry: 1_000_000,
3009 htlc_minimum_msat: 1,
3010 },
3011 payment_context,
3012 }
3013 .authenticate(nonce, &expanded_key);
3014
3015 let payment_path = BlindedPaymentPath::one_hop(
3017 cln_node_public_key,
3018 payee_tlvs.clone(),
3019 params.min_cltv as u16,
3020 &entropy_source,
3021 &secp,
3022 )
3023 .map_err(|_| {
3024 PaymentError::generic(
3025 "Failed to create BOLT12 invoice: Error creating blinded payment path",
3026 )
3027 })?;
3028
3029 let invoice = invoice_request
3031 .respond_with_no_std(
3032 vec![payment_path],
3033 PaymentHash(preimage_hash),
3034 SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e| {
3035 PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3036 })?,
3037 )?
3038 .build()?
3039 .sign(|unsigned_invoice: &UnsignedBolt12Invoice| {
3040 Ok(secp.sign_schnorr_no_aux_rand(unsigned_invoice.as_ref().as_digest(), &keypair))
3041 })
3042 .map_err(|e| {
3043 PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3044 })?;
3045 let invoice_str = encode_invoice(&invoice).map_err(|e| {
3046 PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3047 })?;
3048 debug!("Created BOLT12 invoice: {invoice_str}");
3049
3050 let claim_keypair = utils::generate_keypair();
3051 let receiver_amount_sat = payer_amount_sat - fees_sat;
3052 let webhook_claim_status =
3053 match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
3054 true => RevSwapStates::TransactionConfirmed,
3055 false => RevSwapStates::TransactionMempool,
3056 };
3057 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3058 url,
3059 hash_swap_id: Some(true),
3060 status: Some(vec![webhook_claim_status]),
3061 });
3062
3063 let v2_req = CreateReverseRequest {
3064 from: "BTC".to_string(),
3065 to: "L-BTC".to_string(),
3066 invoice: Some(invoice_str.clone()),
3067 invoice_amount: None,
3068 preimage_hash: None,
3069 claim_public_key: claim_keypair.public_key().into(),
3070 description: None,
3071 description_hash: None,
3072 address: Some(mrh_addr_str.clone()),
3073 address_signature: Some(mrh_addr_hash_sig.to_hex()),
3074 referral_id: None,
3075 webhook,
3076 };
3077 let create_response = self.swapper.create_receive_swap(v2_req).await?;
3078
3079 self.persister.insert_or_update_reserved_address(
3081 &mrh_addr_str,
3082 create_response.timeout_block_height,
3083 )?;
3084
3085 let swap_id = create_response.id.clone();
3086 let destination_pubkey = cln_node_public_key.to_hex();
3087 debug!("Created receive swap: {swap_id}");
3088
3089 let create_response_json =
3090 ReceiveSwap::from_boltz_struct_to_json(&create_response, &swap_id, None)?;
3091 let invoice_description = invoice.description().map(|s| s.to_string());
3092
3093 self.persister
3094 .insert_or_update_receive_swap(&ReceiveSwap {
3095 id: swap_id.clone(),
3096 preimage: preimage_str,
3097 create_response_json,
3098 claim_private_key: claim_keypair.display_secret().to_string(),
3099 invoice: invoice_str.clone(),
3100 bolt12_offer: Some(req.offer.clone()),
3101 payment_hash: Some(preimage.sha256.to_string()),
3102 destination_pubkey: Some(destination_pubkey),
3103 timeout_block_height: create_response.timeout_block_height,
3104 description: invoice_description,
3105 payer_note,
3106 payer_amount_sat,
3107 receiver_amount_sat,
3108 pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
3109 PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
3110 })?,
3111 claim_fees_sat: reverse_pair.fees.claim_estimate(),
3112 lockup_tx_id: None,
3113 claim_address: None,
3114 claim_tx_id: None,
3115 mrh_address: mrh_addr_str,
3116 mrh_tx_id: None,
3117 created_at: utils::now(),
3118 state: PaymentState::Created,
3119 metadata: Default::default(),
3120 })
3121 .map_err(|_| PaymentError::PersistError)?;
3122 self.status_stream.track_swap_id(&swap_id)?;
3123 debug!("Finished create BOLT12 invoice");
3124
3125 Ok(CreateBolt12InvoiceResponse {
3126 invoice: invoice_str,
3127 })
3128 }
3129
3130 async fn create_bolt12_offer(
3131 &self,
3132 description: String,
3133 ) -> Result<ReceivePaymentResponse, PaymentError> {
3134 let webhook_url = self.persister.get_webhook_url()?;
3135 let (nodes, maybe_reverse_pair) = tokio::try_join!(
3137 self.swapper.get_nodes(),
3138 self.swapper.get_reverse_swap_pairs()
3139 )?;
3140 let cln_node = nodes
3141 .get_btc_cln_node()
3142 .ok_or(PaymentError::generic("No BTC CLN node found"))?;
3143 debug!("Creating BOLT12 offer for description: {description}");
3144 let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
3145 let min_amount_sat = reverse_pair.limits.minimal;
3146 let keypair = utils::generate_keypair();
3147 let entropy_source = RandomBytes::new(utils::generate_entropy());
3148 let secp = Secp256k1::new();
3149 let message_context = MessageContext::Offers(OffersContext::InvoiceRequest {
3150 nonce: Nonce::from_entropy_source(&entropy_source),
3151 });
3152
3153 let offer = OfferBuilder::new(keypair.public_key())
3155 .chain(self.config.network.into())
3156 .amount_msats(min_amount_sat * 1_000)
3157 .description(description.clone())
3158 .path(
3159 BlindedMessagePath::one_hop(
3160 cln_node.public_key,
3161 message_context,
3162 &entropy_source,
3163 &secp,
3164 )
3165 .map_err(|_| {
3166 PaymentError::generic(
3167 "Error creating Bolt12 Offer: Could not create a one-hop blinded path",
3168 )
3169 })?,
3170 )
3171 .build()?;
3172 let offer_str = utils::bolt12::encode_offer(&offer)?;
3173 info!("Created BOLT12 offer: {offer_str}");
3174 self.swapper
3175 .create_bolt12_offer(CreateBolt12OfferRequest {
3176 offer: offer_str.clone(),
3177 url: webhook_url.clone(),
3178 })
3179 .await?;
3180 self.persister.insert_or_update_bolt12_offer(&Bolt12Offer {
3182 id: offer_str.clone(),
3183 description,
3184 private_key: keypair.display_secret().to_string(),
3185 webhook_url,
3186 created_at: utils::now(),
3187 })?;
3188 let subscribe_hash_sig = utils::sign_message_hash("SUBSCRIBE", &keypair)?;
3190 self.status_stream
3191 .track_offer(&offer_str, &subscribe_hash_sig.to_hex())?;
3192
3193 Ok(ReceivePaymentResponse {
3194 destination: offer_str,
3195 })
3196 }
3197
3198 async fn create_receive_chain_swap(
3199 &self,
3200 user_lockup_amount_sat: Option<u64>,
3201 fees_sat: u64,
3202 ) -> Result<ChainSwap, PaymentError> {
3203 let pair = self
3204 .get_and_validate_chain_pair(Direction::Incoming, user_lockup_amount_sat)
3205 .await?;
3206 let claim_fees_sat = pair.fees.claim_estimate();
3207 let server_fees_sat = pair.fees.server();
3208 let service_fees_sat = user_lockup_amount_sat
3210 .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
3211 .unwrap_or_default();
3212
3213 ensure_sdk!(
3214 fees_sat == service_fees_sat + claim_fees_sat + server_fees_sat,
3215 PaymentError::InvalidOrExpiredFees
3216 );
3217
3218 let preimage = Preimage::new();
3219 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3220
3221 let claim_keypair = utils::generate_keypair();
3222 let claim_public_key = boltz_client::PublicKey {
3223 compressed: true,
3224 inner: claim_keypair.public_key(),
3225 };
3226 let refund_keypair = utils::generate_keypair();
3227 let refund_public_key = boltz_client::PublicKey {
3228 compressed: true,
3229 inner: refund_keypair.public_key(),
3230 };
3231 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3232 url,
3233 hash_swap_id: Some(true),
3234 status: Some(vec![
3235 ChainSwapStates::TransactionFailed,
3236 ChainSwapStates::TransactionLockupFailed,
3237 ChainSwapStates::TransactionServerConfirmed,
3238 ]),
3239 });
3240 let create_response = self
3241 .swapper
3242 .create_chain_swap(CreateChainRequest {
3243 from: "BTC".to_string(),
3244 to: "L-BTC".to_string(),
3245 preimage_hash: preimage.sha256,
3246 claim_public_key: Some(claim_public_key),
3247 refund_public_key: Some(refund_public_key),
3248 user_lock_amount: user_lockup_amount_sat,
3249 server_lock_amount: None,
3250 pair_hash: Some(pair.hash.clone()),
3251 referral_id: None,
3252 webhook,
3253 })
3254 .await?;
3255
3256 let swap_id = create_response.id.clone();
3257 let create_response_json =
3258 ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?;
3259
3260 let accept_zero_conf = user_lockup_amount_sat
3261 .map(|user_lockup_amount_sat| user_lockup_amount_sat <= pair.limits.maximal_zero_conf)
3262 .unwrap_or(false);
3263 let receiver_amount_sat = user_lockup_amount_sat
3264 .map(|user_lockup_amount_sat| user_lockup_amount_sat - fees_sat)
3265 .unwrap_or(0);
3266
3267 let swap = ChainSwap {
3268 id: swap_id.clone(),
3269 direction: Direction::Incoming,
3270 claim_address: None,
3271 lockup_address: create_response.lockup_details.lockup_address,
3272 refund_address: None,
3273 timeout_block_height: create_response.lockup_details.timeout_block_height,
3274 preimage: preimage_str,
3275 description: Some("Bitcoin transfer".to_string()),
3276 payer_amount_sat: user_lockup_amount_sat.unwrap_or(0),
3277 actual_payer_amount_sat: None,
3278 receiver_amount_sat,
3279 accepted_receiver_amount_sat: None,
3280 claim_fees_sat,
3281 pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
3282 PaymentError::generic(format!("Failed to serialize incoming ChainPair: {e:?}"))
3283 })?,
3284 accept_zero_conf,
3285 create_response_json,
3286 claim_private_key: claim_keypair.display_secret().to_string(),
3287 refund_private_key: refund_keypair.display_secret().to_string(),
3288 server_lockup_tx_id: None,
3289 user_lockup_tx_id: None,
3290 claim_tx_id: None,
3291 refund_tx_id: None,
3292 created_at: utils::now(),
3293 state: PaymentState::Created,
3294 auto_accepted_fees: false,
3295 metadata: Default::default(),
3296 };
3297 self.persister.insert_or_update_chain_swap(&swap)?;
3298 self.status_stream.track_swap_id(&swap.id)?;
3299 Ok(swap)
3300 }
3301
3302 async fn receive_onchain(
3307 &self,
3308 user_lockup_amount_sat: Option<u64>,
3309 fees_sat: u64,
3310 ) -> Result<ReceivePaymentResponse, PaymentError> {
3311 self.ensure_is_started().await?;
3312
3313 let swap = self
3314 .create_receive_chain_swap(user_lockup_amount_sat, fees_sat)
3315 .await?;
3316 let create_response = swap.get_boltz_create_response()?;
3317 let address = create_response.lockup_details.lockup_address;
3318
3319 let amount = create_response.lockup_details.amount as f64 / 100_000_000.0;
3320 let bip21 = create_response.lockup_details.bip21.unwrap_or(format!(
3321 "bitcoin:{address}?amount={amount}&label=Send%20to%20L-BTC%20address"
3322 ));
3323
3324 Ok(ReceivePaymentResponse { destination: bip21 })
3325 }
3326
3327 pub async fn list_refundables(&self) -> SdkResult<Vec<RefundableSwap>> {
3330 let chain_swaps = self.persister.list_refundable_chain_swaps()?;
3331
3332 let mut chain_swaps_with_scripts = vec![];
3333 for swap in &chain_swaps {
3334 let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
3335 chain_swaps_with_scripts.push((swap, script_pubkey));
3336 }
3337
3338 let lockup_scripts: Vec<&boltz_client::bitcoin::Script> = chain_swaps_with_scripts
3339 .iter()
3340 .map(|(_, script_pubkey)| script_pubkey.as_script())
3341 .collect();
3342 let scripts_utxos = self
3343 .bitcoin_chain_service
3344 .get_scripts_utxos(&lockup_scripts)
3345 .await?;
3346
3347 let mut script_to_utxos_map = std::collections::HashMap::new();
3348 for script_utxos in scripts_utxos {
3349 if let Some(first_utxo) = script_utxos.first() {
3350 if let Some((_, txo)) = first_utxo.as_bitcoin() {
3351 let script_pubkey: boltz_client::bitcoin::ScriptBuf = txo.script_pubkey.clone();
3352 script_to_utxos_map.insert(script_pubkey, script_utxos);
3353 }
3354 }
3355 }
3356
3357 let mut refundables = vec![];
3358
3359 for (chain_swap, script_pubkey) in chain_swaps_with_scripts {
3360 if let Some(script_utxos) = script_to_utxos_map.get(&script_pubkey) {
3361 let swap_id = &chain_swap.id;
3362 let amount_sat: u64 = script_utxos
3363 .iter()
3364 .filter_map(|utxo| utxo.as_bitcoin().cloned())
3365 .map(|(_, txo)| txo.value.to_sat())
3366 .sum();
3367 info!("Incoming Chain Swap {swap_id} is refundable with {amount_sat} sats");
3368
3369 refundables.push(chain_swap.to_refundable(amount_sat));
3370 }
3371 }
3372
3373 Ok(refundables)
3374 }
3375
3376 pub async fn prepare_refund(
3385 &self,
3386 req: &PrepareRefundRequest,
3387 ) -> SdkResult<PrepareRefundResponse> {
3388 let refund_address = self
3389 .validate_bitcoin_address(&req.refund_address)
3390 .await
3391 .map_err(|e| SdkError::Generic {
3392 err: format!("Failed to validate refund address: {e}"),
3393 })?;
3394
3395 let (tx_vsize, tx_fee_sat, refund_tx_id) = self
3396 .chain_swap_handler
3397 .prepare_refund(
3398 &req.swap_address,
3399 &refund_address,
3400 req.fee_rate_sat_per_vbyte,
3401 )
3402 .await?;
3403 Ok(PrepareRefundResponse {
3404 tx_vsize,
3405 tx_fee_sat,
3406 last_refund_tx_id: refund_tx_id,
3407 })
3408 }
3409
3410 pub async fn refund(&self, req: &RefundRequest) -> Result<RefundResponse, PaymentError> {
3419 let refund_address = self
3420 .validate_bitcoin_address(&req.refund_address)
3421 .await
3422 .map_err(|e| SdkError::Generic {
3423 err: format!("Failed to validate refund address: {e}"),
3424 })?;
3425
3426 let refund_tx_id = self
3427 .chain_swap_handler
3428 .refund_incoming_swap(
3429 &req.swap_address,
3430 &refund_address,
3431 req.fee_rate_sat_per_vbyte,
3432 true,
3433 )
3434 .or_else(|e| {
3435 warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
3436 self.chain_swap_handler.refund_incoming_swap(
3437 &req.swap_address,
3438 &refund_address,
3439 req.fee_rate_sat_per_vbyte,
3440 false,
3441 )
3442 })
3443 .await?;
3444
3445 Ok(RefundResponse { refund_tx_id })
3446 }
3447
3448 pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> {
3456 let t0 = Instant::now();
3457 let mut rescannable_swaps: Vec<Swap> = self
3458 .persister
3459 .list_chain_swaps()?
3460 .into_iter()
3461 .map(Into::into)
3462 .collect();
3463 self.recoverer
3464 .recover_from_onchain(&mut rescannable_swaps, None)
3465 .await?;
3466 let scanned_len = rescannable_swaps.len();
3467 for swap in rescannable_swaps {
3468 let swap_id = &swap.id();
3469 if let Swap::Chain(chain_swap) = swap {
3470 if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
3471 error!("Error persisting rescanned Chain Swap {swap_id}: {e}");
3472 }
3473 }
3474 }
3475 info!(
3476 "Rescanned {} chain swaps in {} seconds",
3477 scanned_len,
3478 t0.elapsed().as_millis()
3479 );
3480 Ok(())
3481 }
3482
3483 fn validate_buy_bitcoin(&self, amount_sat: u64) -> Result<(), PaymentError> {
3484 ensure_sdk!(
3485 self.config.network == LiquidNetwork::Mainnet,
3486 PaymentError::invalid_network("Can only buy bitcoin on Mainnet")
3487 );
3488 ensure_sdk!(
3490 amount_sat % 1_000 == 0,
3491 PaymentError::generic("Can only buy sat amounts that are multiples of 1000")
3492 );
3493 Ok(())
3494 }
3495
3496 pub async fn prepare_buy_bitcoin(
3504 &self,
3505 req: &PrepareBuyBitcoinRequest,
3506 ) -> Result<PrepareBuyBitcoinResponse, PaymentError> {
3507 self.validate_buy_bitcoin(req.amount_sat)?;
3508
3509 let res = self
3510 .prepare_receive_payment(&PrepareReceiveRequest {
3511 payment_method: PaymentMethod::BitcoinAddress,
3512 amount: Some(ReceiveAmount::Bitcoin {
3513 payer_amount_sat: req.amount_sat,
3514 }),
3515 })
3516 .await?;
3517
3518 let Some(ReceiveAmount::Bitcoin {
3519 payer_amount_sat: amount_sat,
3520 }) = res.amount
3521 else {
3522 return Err(PaymentError::Generic {
3523 err: format!(
3524 "Error preparing receive payment, got amount: {:?}",
3525 res.amount
3526 ),
3527 });
3528 };
3529
3530 Ok(PrepareBuyBitcoinResponse {
3531 provider: req.provider,
3532 amount_sat,
3533 fees_sat: res.fees_sat,
3534 })
3535 }
3536
3537 pub async fn buy_bitcoin(&self, req: &BuyBitcoinRequest) -> Result<String, PaymentError> {
3545 self.validate_buy_bitcoin(req.prepare_response.amount_sat)?;
3546
3547 let swap = self
3548 .create_receive_chain_swap(
3549 Some(req.prepare_response.amount_sat),
3550 req.prepare_response.fees_sat,
3551 )
3552 .await?;
3553
3554 Ok(self
3555 .buy_bitcoin_service
3556 .buy_bitcoin(
3557 req.prepare_response.provider,
3558 &swap,
3559 req.redirect_url.clone(),
3560 )
3561 .await?)
3562 }
3563
3564 pub(crate) async fn get_monitored_swaps_list(
3565 &self,
3566 partial_sync: bool,
3567 chain_tips: ChainTips,
3568 ) -> Result<Vec<Swap>> {
3569 let receive_swaps = self
3570 .persister
3571 .list_recoverable_receive_swaps()?
3572 .into_iter()
3573 .map(Into::into)
3574 .collect();
3575 match partial_sync {
3576 false => {
3577 let final_swap_states = [PaymentState::Complete, PaymentState::Failed];
3578
3579 let send_swaps = self
3580 .persister
3581 .list_recoverable_send_swaps()?
3582 .into_iter()
3583 .map(Into::into)
3584 .collect();
3585 let chain_swaps: Vec<Swap> = self
3586 .persister
3587 .list_chain_swaps()?
3588 .into_iter()
3589 .filter(|swap| match swap.direction {
3590 Direction::Incoming => {
3591 chain_tips.bitcoin_tip
3592 <= swap.timeout_block_height
3593 + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS
3594 }
3595 Direction::Outgoing => {
3596 !final_swap_states.contains(&swap.state)
3597 && chain_tips.liquid_tip <= swap.timeout_block_height
3598 }
3599 })
3600 .map(Into::into)
3601 .collect();
3602 Ok([receive_swaps, send_swaps, chain_swaps].concat())
3603 }
3604 true => Ok(receive_swaps),
3605 }
3606 }
3607
3608 async fn sync_payments_with_chain_data(
3611 &self,
3612 partial_sync: bool,
3613 chain_tips: ChainTips,
3614 ) -> Result<()> {
3615 let mut recoverable_swaps = self
3616 .get_monitored_swaps_list(partial_sync, chain_tips)
3617 .await?;
3618 let mut wallet_tx_map = self
3619 .recoverer
3620 .recover_from_onchain(&mut recoverable_swaps, Some(chain_tips))
3621 .await?;
3622
3623 let all_wallet_tx_ids: HashSet<String> =
3624 wallet_tx_map.keys().map(|txid| txid.to_string()).collect();
3625
3626 for swap in recoverable_swaps {
3627 let swap_id = &swap.id();
3628
3629 match swap {
3631 Swap::Receive(receive_swap) => {
3632 let history_updates = vec![&receive_swap.claim_tx_id, &receive_swap.mrh_tx_id];
3633 for tx_id in history_updates
3634 .into_iter()
3635 .flatten()
3636 .collect::<Vec<&String>>()
3637 {
3638 if let Some(tx) =
3639 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
3640 {
3641 self.persister
3642 .insert_or_update_payment_with_wallet_tx(&tx)?;
3643 }
3644 }
3645 if let Err(e) = self.receive_swap_handler.update_swap(receive_swap) {
3646 error!("Error persisting recovered receive swap {swap_id}: {e}");
3647 }
3648 }
3649 Swap::Send(send_swap) => {
3650 let history_updates = vec![&send_swap.lockup_tx_id, &send_swap.refund_tx_id];
3651 for tx_id in history_updates
3652 .into_iter()
3653 .flatten()
3654 .collect::<Vec<&String>>()
3655 {
3656 if let Some(tx) =
3657 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
3658 {
3659 self.persister
3660 .insert_or_update_payment_with_wallet_tx(&tx)?;
3661 }
3662 }
3663 if let Err(e) = self.send_swap_handler.update_swap(send_swap) {
3664 error!("Error persisting recovered send swap {swap_id}: {e}");
3665 }
3666 }
3667 Swap::Chain(chain_swap) => {
3668 let history_updates = match chain_swap.direction {
3669 Direction::Incoming => vec![&chain_swap.claim_tx_id],
3670 Direction::Outgoing => {
3671 vec![&chain_swap.user_lockup_tx_id, &chain_swap.refund_tx_id]
3672 }
3673 };
3674 for tx_id in history_updates
3675 .into_iter()
3676 .flatten()
3677 .collect::<Vec<&String>>()
3678 {
3679 if let Some(tx) =
3680 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
3681 {
3682 self.persister
3683 .insert_or_update_payment_with_wallet_tx(&tx)?;
3684 }
3685 }
3686 if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
3687 error!("Error persisting recovered Chain Swap {swap_id}: {e}");
3688 }
3689 }
3690 };
3691 }
3692
3693 let non_swap_wallet_tx_map = wallet_tx_map;
3694
3695 let payments = self
3696 .persister
3697 .get_payments_by_tx_id(&ListPaymentsRequest::default())?;
3698
3699 let unconfirmed_payment_txs_data = self.persister.list_unconfirmed_payment_txs_data()?;
3701 let unconfirmed_txs_by_id: HashMap<String, PaymentTxData> = unconfirmed_payment_txs_data
3702 .into_iter()
3703 .map(|tx| (tx.tx_id.clone(), tx))
3704 .collect::<HashMap<String, PaymentTxData>>();
3705
3706 for tx in non_swap_wallet_tx_map.values() {
3707 let tx_id = tx.txid.to_string();
3708 let maybe_payment = payments.get(&tx_id);
3709 let mut updated = false;
3710 match maybe_payment {
3711 None
3713 | Some(Payment {
3714 details: PaymentDetails::Liquid { .. },
3715 ..
3716 }) => {
3717 let updated_needed = maybe_payment
3718 .is_none_or(|payment| payment.status == Pending && tx.height.is_some());
3719 if updated_needed {
3720 self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
3723 self.emit_payment_updated(Some(tx_id.clone())).await?;
3724 updated = true
3725 }
3726 }
3727
3728 _ => {}
3729 }
3730 if !updated && unconfirmed_txs_by_id.contains_key(&tx_id) && tx.height.is_some() {
3731 self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
3733 }
3734 }
3735
3736 let unknown_unconfirmed_txs: Vec<_> = unconfirmed_txs_by_id
3737 .iter()
3738 .filter(|(txid, _)| !all_wallet_tx_ids.contains(*txid))
3739 .map(|(_, tx)| tx)
3740 .collect();
3741
3742 for unknown_unconfirmed_tx in unknown_unconfirmed_txs {
3743 if unknown_unconfirmed_tx.timestamp.is_some_and(|t| {
3744 (utils::now().saturating_sub(t)) > NETWORK_PROPAGATION_GRACE_PERIOD.as_secs() as u32
3745 }) {
3746 self.persister
3747 .delete_payment_tx_data(&unknown_unconfirmed_tx.tx_id)?;
3748 info!(
3749 "Found an unknown unconfirmed tx and deleted it. Txid: {}",
3750 unknown_unconfirmed_tx.tx_id
3751 );
3752 } else {
3753 debug!(
3754 "Found an unknown unconfirmed tx that was inserted at {:?}. \
3755 Keeping it to allow propagation through the network. Txid: {}",
3756 unknown_unconfirmed_tx.timestamp, unknown_unconfirmed_tx.tx_id
3757 )
3758 }
3759 }
3760
3761 self.update_wallet_info().await?;
3762 Ok(())
3763 }
3764
3765 async fn update_wallet_info(&self) -> Result<()> {
3766 let asset_metadata: HashMap<String, AssetMetadata> = self
3767 .persister
3768 .list_asset_metadata()?
3769 .into_iter()
3770 .map(|am| (am.asset_id.clone(), am))
3771 .collect();
3772 let transactions = self.onchain_wallet.transactions().await?;
3773 let tx_ids = transactions
3774 .iter()
3775 .map(|tx| tx.txid.to_string())
3776 .collect::<Vec<_>>();
3777 let asset_balances = transactions
3778 .into_iter()
3779 .fold(BTreeMap::<AssetId, i64>::new(), |mut acc, tx| {
3780 tx.balance.into_iter().for_each(|(asset_id, balance)| {
3781 if tx.height.is_some() || balance < 0 {
3783 *acc.entry(asset_id).or_default() += balance;
3784 }
3785 });
3786 acc
3787 })
3788 .into_iter()
3789 .map(|(asset_id, balance)| {
3790 let asset_id = asset_id.to_hex();
3791 let balance_sat = balance.unsigned_abs();
3792 let maybe_asset_metadata = asset_metadata.get(&asset_id);
3793 AssetBalance {
3794 asset_id,
3795 balance_sat,
3796 name: maybe_asset_metadata.map(|am| am.name.clone()),
3797 ticker: maybe_asset_metadata.map(|am| am.ticker.clone()),
3798 balance: maybe_asset_metadata.map(|am| am.amount_from_sat(balance_sat)),
3799 }
3800 })
3801 .collect::<Vec<AssetBalance>>();
3802 let mut balance_sat = asset_balances
3803 .clone()
3804 .into_iter()
3805 .find(|ab| ab.asset_id.eq(&self.config.lbtc_asset_id()))
3806 .map_or(0, |ab| ab.balance_sat);
3807
3808 let mut pending_send_sat = 0;
3809 let mut pending_receive_sat = 0;
3810 let payments = self.persister.get_payments(&ListPaymentsRequest {
3811 states: Some(vec![
3812 PaymentState::Pending,
3813 PaymentState::RefundPending,
3814 PaymentState::WaitingFeeAcceptance,
3815 ]),
3816 ..Default::default()
3817 })?;
3818
3819 for payment in payments {
3820 let is_lbtc_asset_id = payment.details.is_lbtc_asset_id(self.config.network);
3821 match payment.payment_type {
3822 PaymentType::Send => match payment.details.get_refund_tx_amount_sat() {
3823 Some(refund_tx_amount_sat) => pending_receive_sat += refund_tx_amount_sat,
3824 None => {
3825 let total_sat = if is_lbtc_asset_id {
3826 payment.amount_sat + payment.fees_sat
3827 } else {
3828 payment.fees_sat
3829 };
3830 if let Some(tx_id) = payment.tx_id {
3831 if !tx_ids.contains(&tx_id) {
3832 debug!("Deducting {total_sat} sats from balance");
3833 balance_sat = balance_sat.saturating_sub(total_sat);
3834 }
3835 }
3836 pending_send_sat += total_sat
3837 }
3838 },
3839 PaymentType::Receive => {
3840 if is_lbtc_asset_id {
3841 pending_receive_sat += payment.amount_sat;
3842 }
3843 }
3844 }
3845 }
3846
3847 debug!("Onchain wallet balance: {balance_sat} sats");
3848 let info_response = WalletInfo {
3849 balance_sat,
3850 pending_send_sat,
3851 pending_receive_sat,
3852 fingerprint: self.onchain_wallet.fingerprint()?,
3853 pubkey: self.onchain_wallet.pubkey()?,
3854 asset_balances,
3855 };
3856 self.persister.set_wallet_info(&info_response)
3857 }
3858
3859 pub async fn list_payments(
3862 &self,
3863 req: &ListPaymentsRequest,
3864 ) -> Result<Vec<Payment>, PaymentError> {
3865 self.ensure_is_started().await?;
3866
3867 Ok(self.persister.get_payments(req)?)
3868 }
3869
3870 pub async fn get_payment(
3881 &self,
3882 req: &GetPaymentRequest,
3883 ) -> Result<Option<Payment>, PaymentError> {
3884 self.ensure_is_started().await?;
3885
3886 Ok(self.persister.get_payment_by_request(req)?)
3887 }
3888
3889 pub async fn fetch_payment_proposed_fees(
3894 &self,
3895 req: &FetchPaymentProposedFeesRequest,
3896 ) -> SdkResult<FetchPaymentProposedFeesResponse> {
3897 let chain_swap =
3898 self.persister
3899 .fetch_chain_swap_by_id(&req.swap_id)?
3900 .ok_or(SdkError::Generic {
3901 err: format!("Could not find Swap {}", req.swap_id),
3902 })?;
3903
3904 ensure_sdk!(
3905 chain_swap.state == WaitingFeeAcceptance,
3906 SdkError::Generic {
3907 err: "Payment is not WaitingFeeAcceptance".to_string()
3908 }
3909 );
3910
3911 let server_lockup_quote = self
3912 .swapper
3913 .get_zero_amount_chain_swap_quote(&req.swap_id)
3914 .await?;
3915
3916 let actual_payer_amount_sat =
3917 chain_swap
3918 .actual_payer_amount_sat
3919 .ok_or(SdkError::Generic {
3920 err: "No actual payer amount found when state is WaitingFeeAcceptance"
3921 .to_string(),
3922 })?;
3923 let fees_sat =
3924 actual_payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat;
3925
3926 Ok(FetchPaymentProposedFeesResponse {
3927 swap_id: req.swap_id.clone(),
3928 fees_sat,
3929 payer_amount_sat: actual_payer_amount_sat,
3930 receiver_amount_sat: actual_payer_amount_sat - fees_sat,
3931 })
3932 }
3933
3934 pub async fn accept_payment_proposed_fees(
3938 &self,
3939 req: &AcceptPaymentProposedFeesRequest,
3940 ) -> Result<(), PaymentError> {
3941 let FetchPaymentProposedFeesResponse {
3942 swap_id,
3943 fees_sat,
3944 payer_amount_sat,
3945 ..
3946 } = req.clone().response;
3947
3948 let chain_swap =
3949 self.persister
3950 .fetch_chain_swap_by_id(&swap_id)?
3951 .ok_or(SdkError::Generic {
3952 err: format!("Could not find Swap {}", swap_id),
3953 })?;
3954
3955 ensure_sdk!(
3956 chain_swap.state == WaitingFeeAcceptance,
3957 PaymentError::Generic {
3958 err: "Payment is not WaitingFeeAcceptance".to_string()
3959 }
3960 );
3961
3962 let server_lockup_quote = self
3963 .swapper
3964 .get_zero_amount_chain_swap_quote(&swap_id)
3965 .await?;
3966
3967 ensure_sdk!(
3968 fees_sat == payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat,
3969 PaymentError::InvalidOrExpiredFees
3970 );
3971
3972 self.persister
3973 .update_accepted_receiver_amount(&swap_id, Some(payer_amount_sat - fees_sat))?;
3974 self.swapper
3975 .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())
3976 .inspect_err(|e| {
3977 error!("Failed to accept zero-amount swap {swap_id} quote: {e} - trying to erase the accepted receiver amount...");
3978 let _ = self
3979 .persister
3980 .update_accepted_receiver_amount(&swap_id, None);
3981 }).await?;
3982 self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
3983 swap_id,
3984 to_state: Pending,
3985 ..Default::default()
3986 })
3987 }
3988
3989 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
3991 pub fn empty_wallet_cache(&self) -> Result<()> {
3992 let mut path = PathBuf::from(self.config.working_dir.clone());
3993 path.push(Into::<lwk_wollet::ElementsNetwork>::into(self.config.network).as_str());
3994 path.push("enc_cache");
3995
3996 std::fs::remove_dir_all(&path)?;
3997 std::fs::create_dir_all(path)?;
3998
3999 Ok(())
4000 }
4001
4002 pub async fn sync(&self, partial_sync: bool) -> SdkResult<()> {
4004 self.sync_inner(partial_sync, None).await
4005 }
4006
4007 async fn sync_inner(
4008 &self,
4009 partial_sync: bool,
4010 maybe_chain_tips: Option<ChainTips>,
4011 ) -> SdkResult<()> {
4012 self.ensure_is_started().await?;
4013
4014 let t0 = Instant::now();
4015
4016 self.onchain_wallet.full_scan().await.map_err(|err| {
4017 error!("Failed to scan wallet: {err:?}");
4018 SdkError::generic(err.to_string())
4019 })?;
4020
4021 let chain_tips = match maybe_chain_tips {
4022 None => ChainTips {
4023 liquid_tip: self.liquid_chain_service.tip().await?,
4024 bitcoin_tip: self.bitcoin_chain_service.tip().await?,
4025 },
4026 Some(tips) => tips,
4027 };
4028
4029 let is_first_sync = !self
4030 .persister
4031 .get_is_first_sync_complete()?
4032 .unwrap_or(false);
4033 match is_first_sync {
4034 true => {
4035 self.event_manager.pause_notifications();
4036 self.sync_payments_with_chain_data(partial_sync, chain_tips)
4037 .await?;
4038 self.event_manager.resume_notifications();
4039 self.persister.set_is_first_sync_complete(true)?;
4040 }
4041 false => {
4042 self.sync_payments_with_chain_data(partial_sync, chain_tips)
4043 .await?;
4044 }
4045 }
4046 let duration_ms = Instant::now().duration_since(t0).as_millis();
4047 info!("Synchronized (partial: {partial_sync}) with mempool and onchain data ({duration_ms} ms)");
4048
4049 self.notify_event_listeners(SdkEvent::Synced).await;
4050 Ok(())
4051 }
4052
4053 pub fn backup(&self, req: BackupRequest) -> Result<()> {
4060 let backup_path = req
4061 .backup_path
4062 .map(PathBuf::from)
4063 .unwrap_or(self.persister.get_default_backup_path());
4064 self.persister.backup(backup_path)
4065 }
4066
4067 pub fn restore(&self, req: RestoreRequest) -> Result<()> {
4074 let backup_path = req
4075 .backup_path
4076 .map(PathBuf::from)
4077 .unwrap_or(self.persister.get_default_backup_path());
4078 ensure_sdk!(
4079 backup_path.exists(),
4080 SdkError::generic("Backup file does not exist").into()
4081 );
4082 self.persister.restore_from_backup(backup_path)
4083 }
4084
4085 pub async fn prepare_lnurl_pay(
4118 &self,
4119 req: PrepareLnUrlPayRequest,
4120 ) -> Result<PrepareLnUrlPayResponse, LnUrlPayError> {
4121 let amount_msat = match req.amount {
4122 PayAmount::Drain => {
4123 let get_info_res = self
4124 .get_info()
4125 .await
4126 .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
4127 ensure_sdk!(
4128 get_info_res.wallet_info.pending_receive_sat == 0
4129 && get_info_res.wallet_info.pending_send_sat == 0,
4130 LnUrlPayError::Generic {
4131 err: "Cannot drain while there are pending payments".to_string(),
4132 }
4133 );
4134 let lbtc_pair = self
4135 .swapper
4136 .get_submarine_pairs()
4137 .await?
4138 .ok_or(PaymentError::PairsNotFound)?;
4139 let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
4140 let drain_amount_sat = get_info_res.wallet_info.balance_sat - drain_fees_sat;
4141 let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
4143 let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
4144 let receiver_amount_sat = utils::increment_receiver_amount_up_to_drain_amount(
4145 dummy_amount_sat,
4146 &lbtc_pair,
4147 drain_amount_sat,
4148 );
4149 lbtc_pair
4150 .limits
4151 .within(receiver_amount_sat)
4152 .map_err(|e| LnUrlPayError::Generic { err: e.message() })?;
4153 let pair_fees_sat = lbtc_pair.fees.total(receiver_amount_sat);
4155 ensure_sdk!(
4156 receiver_amount_sat + pair_fees_sat == drain_amount_sat,
4157 LnUrlPayError::Generic {
4158 err: "Cannot drain without leaving a remainder".to_string(),
4159 }
4160 );
4161
4162 receiver_amount_sat * 1000
4163 }
4164 PayAmount::Bitcoin {
4165 receiver_amount_sat,
4166 } => receiver_amount_sat * 1000,
4167 PayAmount::Asset { .. } => {
4168 return Err(LnUrlPayError::Generic {
4169 err: "Cannot send an asset to a Bitcoin address".to_string(),
4170 })
4171 }
4172 };
4173
4174 match validate_lnurl_pay(
4175 self.rest_client.as_ref(),
4176 amount_msat,
4177 &req.comment,
4178 &req.data,
4179 self.config.network.into(),
4180 req.validate_success_action_url,
4181 )
4182 .await?
4183 {
4184 ValidatedCallbackResponse::EndpointError { data } => {
4185 Err(LnUrlPayError::Generic { err: data.reason })
4186 }
4187 ValidatedCallbackResponse::EndpointSuccess { data } => {
4188 let prepare_response = self
4189 .prepare_send_payment(&PrepareSendRequest {
4190 destination: data.pr.clone(),
4191 amount: Some(req.amount.clone()),
4192 })
4193 .await
4194 .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
4195
4196 let destination = match prepare_response.destination {
4197 SendDestination::Bolt11 { invoice, .. } => SendDestination::Bolt11 {
4198 invoice,
4199 bip353_address: req.bip353_address,
4200 },
4201 SendDestination::LiquidAddress { address_data, .. } => {
4202 SendDestination::LiquidAddress {
4203 address_data,
4204 bip353_address: req.bip353_address,
4205 }
4206 }
4207 destination => destination,
4208 };
4209 let fees_sat = prepare_response
4210 .fees_sat
4211 .ok_or(PaymentError::InsufficientFunds)?;
4212
4213 Ok(PrepareLnUrlPayResponse {
4214 destination,
4215 fees_sat,
4216 data: req.data,
4217 amount: req.amount,
4218 comment: req.comment,
4219 success_action: data.success_action,
4220 })
4221 }
4222 }
4223 }
4224
4225 pub async fn lnurl_pay(
4238 &self,
4239 req: model::LnUrlPayRequest,
4240 ) -> Result<LnUrlPayResult, LnUrlPayError> {
4241 let prepare_response = req.prepare_response;
4242 let mut payment = self
4243 .send_payment(&SendPaymentRequest {
4244 prepare_response: PrepareSendResponse {
4245 destination: prepare_response.destination.clone(),
4246 fees_sat: Some(prepare_response.fees_sat),
4247 estimated_asset_fees: None,
4248 amount: Some(prepare_response.amount),
4249 },
4250 use_asset_fees: None,
4251 payer_note: prepare_response.comment.clone(),
4252 })
4253 .await
4254 .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?
4255 .payment;
4256
4257 let maybe_sa_processed: Option<SuccessActionProcessed> = match prepare_response
4258 .success_action
4259 .clone()
4260 {
4261 Some(sa) => {
4262 match sa {
4263 SuccessAction::Aes { data } => {
4265 let PaymentDetails::Lightning {
4266 swap_id, preimage, ..
4267 } = &payment.details
4268 else {
4269 return Err(LnUrlPayError::Generic {
4270 err: format!("Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.", payment.details),
4271 });
4272 };
4273
4274 match preimage {
4275 Some(preimage_str) => {
4276 debug!(
4277 "Decrypting AES success action with preimage for Send Swap {}",
4278 swap_id
4279 );
4280 let preimage =
4281 sha256::Hash::from_str(preimage_str).map_err(|_| {
4282 LnUrlPayError::Generic {
4283 err: "Invalid preimage".to_string(),
4284 }
4285 })?;
4286 let preimage_arr = preimage.to_byte_array();
4287 let result = match (data, &preimage_arr).try_into() {
4288 Ok(data) => AesSuccessActionDataResult::Decrypted { data },
4289 Err(e) => AesSuccessActionDataResult::ErrorStatus {
4290 reason: e.to_string(),
4291 },
4292 };
4293 Some(SuccessActionProcessed::Aes { result })
4294 }
4295 None => {
4296 debug!("Preimage not yet available to decrypt AES success action for Send Swap {}", swap_id);
4297 None
4298 }
4299 }
4300 }
4301 SuccessAction::Message { data } => {
4302 Some(SuccessActionProcessed::Message { data })
4303 }
4304 SuccessAction::Url { data } => Some(SuccessActionProcessed::Url { data }),
4305 }
4306 }
4307 None => None,
4308 };
4309
4310 let description = payment
4311 .details
4312 .get_description()
4313 .or_else(|| extract_description_from_metadata(&prepare_response.data));
4314
4315 let lnurl_pay_domain = match prepare_response.data.ln_address {
4316 Some(_) => None,
4317 None => Some(prepare_response.data.domain),
4318 };
4319 if let (Some(tx_id), Some(destination)) =
4320 (payment.tx_id.clone(), payment.destination.clone())
4321 {
4322 self.persister
4323 .insert_or_update_payment_details(PaymentTxDetails {
4324 tx_id: tx_id.clone(),
4325 destination,
4326 description,
4327 lnurl_info: Some(LnUrlInfo {
4328 ln_address: prepare_response.data.ln_address,
4329 lnurl_pay_comment: prepare_response.comment,
4330 lnurl_pay_domain,
4331 lnurl_pay_metadata: Some(prepare_response.data.metadata_str),
4332 lnurl_pay_success_action: maybe_sa_processed.clone(),
4333 lnurl_pay_unprocessed_success_action: prepare_response.success_action,
4334 lnurl_withdraw_endpoint: None,
4335 }),
4336 ..Default::default()
4337 })?;
4338 payment = self.persister.get_payment(&tx_id)?.unwrap_or(payment);
4340 }
4341
4342 Ok(LnUrlPayResult::EndpointSuccess {
4343 data: model::LnUrlPaySuccessData {
4344 payment,
4345 success_action: maybe_sa_processed,
4346 },
4347 })
4348 }
4349
4350 pub async fn lnurl_withdraw(
4357 &self,
4358 req: LnUrlWithdrawRequest,
4359 ) -> Result<LnUrlWithdrawResult, LnUrlWithdrawError> {
4360 let prepare_response = self
4361 .prepare_receive_payment(&{
4362 PrepareReceiveRequest {
4363 payment_method: PaymentMethod::Bolt11Invoice,
4364 amount: Some(ReceiveAmount::Bitcoin {
4365 payer_amount_sat: req.amount_msat / 1_000,
4366 }),
4367 }
4368 })
4369 .await?;
4370 let receive_res = self
4371 .receive_payment(&ReceivePaymentRequest {
4372 prepare_response,
4373 description: req.description.clone(),
4374 use_description_hash: Some(false),
4375 payer_note: None,
4376 })
4377 .await?;
4378
4379 let Ok(invoice) = parse_invoice(&receive_res.destination) else {
4380 return Err(LnUrlWithdrawError::Generic {
4381 err: "Received unexpected output from receive request".to_string(),
4382 });
4383 };
4384
4385 let res =
4386 validate_lnurl_withdraw(self.rest_client.as_ref(), req.data.clone(), invoice.clone())
4387 .await?;
4388 if let LnUrlWithdrawResult::Ok { data: _ } = res {
4389 if let Some(ReceiveSwap {
4390 claim_tx_id: Some(tx_id),
4391 ..
4392 }) = self
4393 .persister
4394 .fetch_receive_swap_by_invoice(&invoice.bolt11)?
4395 {
4396 self.persister
4397 .insert_or_update_payment_details(PaymentTxDetails {
4398 tx_id,
4399 destination: receive_res.destination,
4400 description: req.description,
4401 lnurl_info: Some(LnUrlInfo {
4402 lnurl_withdraw_endpoint: Some(req.data.callback),
4403 ..Default::default()
4404 }),
4405 ..Default::default()
4406 })?;
4407 }
4408 }
4409 Ok(res)
4410 }
4411
4412 pub async fn lnurl_auth(
4418 &self,
4419 req_data: LnUrlAuthRequestData,
4420 ) -> Result<LnUrlCallbackStatus, LnUrlAuthError> {
4421 Ok(perform_lnurl_auth(
4422 self.rest_client.as_ref(),
4423 &req_data,
4424 &SdkLnurlAuthSigner::new(self.signer.clone()),
4425 )
4426 .await?)
4427 }
4428
4429 pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> {
4437 info!("Registering for webhook notifications");
4438 self.persister.set_webhook_url(webhook_url.clone())?;
4439
4440 let bolt12_offers = self.persister.list_bolt12_offers()?;
4442 for mut bolt12_offer in bolt12_offers {
4443 if bolt12_offer
4444 .webhook_url
4445 .clone()
4446 .is_none_or(|url| url != webhook_url)
4447 {
4448 let keypair = bolt12_offer.get_keypair()?;
4449 let webhook_url_hash_sig = utils::sign_message_hash(&webhook_url, &keypair)?;
4450 self.swapper
4451 .update_bolt12_offer(UpdateBolt12OfferRequest {
4452 offer: bolt12_offer.id.clone(),
4453 url: Some(webhook_url.clone()),
4454 signature: webhook_url_hash_sig.to_hex(),
4455 })
4456 .await?;
4457 bolt12_offer.webhook_url = Some(webhook_url.clone());
4458 self.persister
4459 .insert_or_update_bolt12_offer(&bolt12_offer)?;
4460 }
4461 }
4462
4463 Ok(())
4464 }
4465
4466 pub async fn unregister_webhook(&self) -> SdkResult<()> {
4473 info!("Unregistering for webhook notifications");
4474 let maybe_old_webhook_url = self.persister.get_webhook_url()?;
4475
4476 self.persister.remove_webhook_url()?;
4477
4478 if let Some(old_webhook_url) = maybe_old_webhook_url {
4480 let bolt12_offers = self
4481 .persister
4482 .list_bolt12_offers_by_webhook_url(&old_webhook_url)?;
4483 for mut bolt12_offer in bolt12_offers {
4484 let keypair = bolt12_offer.get_keypair()?;
4485 let update_hash_sig = utils::sign_message_hash("UPDATE", &keypair)?;
4486 self.swapper
4487 .update_bolt12_offer(UpdateBolt12OfferRequest {
4488 offer: bolt12_offer.id.clone(),
4489 url: None,
4490 signature: update_hash_sig.to_hex(),
4491 })
4492 .await?;
4493 bolt12_offer.webhook_url = None;
4494 self.persister
4495 .insert_or_update_bolt12_offer(&bolt12_offer)?;
4496 }
4497 }
4498
4499 Ok(())
4500 }
4501
4502 pub async fn fetch_fiat_rates(&self) -> Result<Vec<Rate>, SdkError> {
4504 self.fiat_api.fetch_fiat_rates().await.map_err(Into::into)
4505 }
4506
4507 pub async fn list_fiat_currencies(&self) -> Result<Vec<FiatCurrency>, SdkError> {
4510 self.fiat_api
4511 .list_fiat_currencies()
4512 .await
4513 .map_err(Into::into)
4514 }
4515
4516 pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
4518 Ok(self.bitcoin_chain_service.recommended_fees().await?)
4519 }
4520
4521 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4522 pub fn default_config(
4524 network: LiquidNetwork,
4525 breez_api_key: Option<String>,
4526 ) -> Result<Config, SdkError> {
4527 let config = match network {
4528 LiquidNetwork::Mainnet => Config::mainnet_esplora(breez_api_key),
4529 LiquidNetwork::Testnet => Config::testnet_esplora(breez_api_key),
4530 LiquidNetwork::Regtest => Config::regtest_esplora(),
4531 };
4532
4533 Ok(config)
4534 }
4535
4536 pub async fn parse(&self, input: &str) -> Result<InputType, PaymentError> {
4540 let external_parsers = &self.external_input_parsers;
4541 let input_type =
4542 parse_with_rest_client(self.rest_client.as_ref(), input, Some(external_parsers))
4543 .await
4544 .map_err(|e| PaymentError::generic(e.to_string()))?;
4545
4546 let res = match input_type {
4547 InputType::LiquidAddress { ref address } => match &address.asset_id {
4548 Some(asset_id) if asset_id.ne(&self.config.lbtc_asset_id()) => {
4549 let asset_metadata = self.persister.get_asset_metadata(asset_id)?.ok_or(
4550 PaymentError::AssetError {
4551 err: format!("Asset {asset_id} is not supported"),
4552 },
4553 )?;
4554 let mut address = address.clone();
4555 address.set_amount_precision(asset_metadata.precision.into());
4556 InputType::LiquidAddress { address }
4557 }
4558 _ => input_type,
4559 },
4560 _ => input_type,
4561 };
4562 Ok(res)
4563 }
4564
4565 pub fn parse_invoice(input: &str) -> Result<LNInvoice, PaymentError> {
4567 parse_invoice(input).map_err(|e| PaymentError::invalid_invoice(e.to_string()))
4568 }
4569
4570 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4594 pub fn init_logging(log_dir: &str, app_logger: Option<Box<dyn log::Log>>) -> Result<()> {
4595 crate::logger::init_logging(log_dir, app_logger)
4596 }
4597}
4598
4599fn extract_description_from_metadata(request_data: &LnUrlPayRequestData) -> Option<String> {
4601 let metadata = request_data.metadata_vec().ok()?;
4602 metadata
4603 .iter()
4604 .find(|item| item.key == "text/plain")
4605 .map(|item| {
4606 info!("Extracted payment description: '{}'", item.value);
4607 item.value.clone()
4608 })
4609}
4610
4611#[cfg(test)]
4612mod tests {
4613 use std::str::FromStr;
4614 use std::time::Duration;
4615
4616 use anyhow::{anyhow, Result};
4617 use boltz_client::{
4618 boltz::{self, TransactionInfo},
4619 swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates},
4620 Secp256k1,
4621 };
4622 use lwk_wollet::{bitcoin::Network, hashes::hex::DisplayHex as _};
4623 use sdk_common::{
4624 bitcoin::hashes::hex::ToHex,
4625 lightning_with_bolt12::{
4626 ln::{channelmanager::PaymentId, inbound_payment::ExpandedKey},
4627 offers::{nonce::Nonce, offer::Offer},
4628 sign::RandomBytes,
4629 util::ser::Writeable,
4630 },
4631 utils::Arc,
4632 };
4633 use tokio_with_wasm::alias as tokio;
4634
4635 use crate::test_utils::swapper::ZeroAmountSwapMockConfig;
4636 use crate::test_utils::wallet::TEST_LIQUID_RECEIVE_LOCKUP_TX;
4637 use crate::utils;
4638 use crate::{
4639 bitcoin, elements,
4640 model::{BtcHistory, Direction, LBtcHistory, PaymentState, Swap},
4641 sdk::LiquidSdk,
4642 test_utils::{
4643 chain::{MockBitcoinChainService, MockLiquidChainService},
4644 chain_swap::{new_chain_swap, TEST_BITCOIN_INCOMING_USER_LOCKUP_TX},
4645 persist::{create_persister, new_receive_swap, new_send_swap},
4646 sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services},
4647 status_stream::MockStatusStream,
4648 swapper::MockSwapper,
4649 },
4650 };
4651 use crate::{
4652 model::CreateBolt12InvoiceRequest,
4653 test_utils::chain_swap::{
4654 TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX, TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX,
4655 TEST_LIQUID_OUTGOING_USER_LOCKUP_TX,
4656 },
4657 };
4658 use paste::paste;
4659
4660 #[cfg(feature = "browser-tests")]
4661 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
4662
4663 struct NewSwapArgs {
4664 direction: Direction,
4665 accepts_zero_conf: bool,
4666 initial_payment_state: Option<PaymentState>,
4667 receiver_amount_sat: Option<u64>,
4668 user_lockup_tx_id: Option<String>,
4669 zero_amount: bool,
4670 set_actual_payer_amount: bool,
4671 }
4672
4673 impl Default for NewSwapArgs {
4674 fn default() -> Self {
4675 Self {
4676 accepts_zero_conf: false,
4677 initial_payment_state: None,
4678 direction: Direction::Outgoing,
4679 receiver_amount_sat: None,
4680 user_lockup_tx_id: None,
4681 zero_amount: false,
4682 set_actual_payer_amount: false,
4683 }
4684 }
4685 }
4686
4687 impl NewSwapArgs {
4688 pub fn set_direction(mut self, direction: Direction) -> Self {
4689 self.direction = direction;
4690 self
4691 }
4692
4693 pub fn set_accepts_zero_conf(mut self, accepts_zero_conf: bool) -> Self {
4694 self.accepts_zero_conf = accepts_zero_conf;
4695 self
4696 }
4697
4698 pub fn set_receiver_amount_sat(mut self, receiver_amount_sat: Option<u64>) -> Self {
4699 self.receiver_amount_sat = receiver_amount_sat;
4700 self
4701 }
4702
4703 pub fn set_user_lockup_tx_id(mut self, user_lockup_tx_id: Option<String>) -> Self {
4704 self.user_lockup_tx_id = user_lockup_tx_id;
4705 self
4706 }
4707
4708 pub fn set_initial_payment_state(mut self, payment_state: PaymentState) -> Self {
4709 self.initial_payment_state = Some(payment_state);
4710 self
4711 }
4712
4713 pub fn set_zero_amount(mut self, zero_amount: bool) -> Self {
4714 self.zero_amount = zero_amount;
4715 self
4716 }
4717
4718 pub fn set_set_actual_payer_amount(mut self, set_actual_payer_amount: bool) -> Self {
4719 self.set_actual_payer_amount = set_actual_payer_amount;
4720 self
4721 }
4722 }
4723
4724 macro_rules! trigger_swap_update {
4725 (
4726 $type:literal,
4727 $args:expr,
4728 $persister:expr,
4729 $status_stream:expr,
4730 $status:expr,
4731 $transaction:expr,
4732 $zero_conf_rejected:expr
4733 ) => {{
4734 let swap = match $type {
4735 "chain" => {
4736 let swap = new_chain_swap(
4737 $args.direction,
4738 $args.initial_payment_state,
4739 $args.accepts_zero_conf,
4740 $args.user_lockup_tx_id,
4741 $args.zero_amount,
4742 $args.set_actual_payer_amount,
4743 $args.receiver_amount_sat,
4744 );
4745 $persister.insert_or_update_chain_swap(&swap).unwrap();
4746 Swap::Chain(swap)
4747 }
4748 "send" => {
4749 let swap =
4750 new_send_swap($args.initial_payment_state, $args.receiver_amount_sat);
4751 $persister.insert_or_update_send_swap(&swap).unwrap();
4752 Swap::Send(swap)
4753 }
4754 "receive" => {
4755 let swap =
4756 new_receive_swap($args.initial_payment_state, $args.receiver_amount_sat);
4757 $persister.insert_or_update_receive_swap(&swap).unwrap();
4758 Swap::Receive(swap)
4759 }
4760 _ => panic!(),
4761 };
4762
4763 $status_stream
4764 .clone()
4765 .send_mock_update(boltz::SwapStatus {
4766 id: swap.id(),
4767 status: $status.to_string(),
4768 transaction: $transaction,
4769 zero_conf_rejected: $zero_conf_rejected,
4770 ..Default::default()
4771 })
4772 .await
4773 .unwrap();
4774
4775 paste! {
4776 $persister.[<fetch _ $type _swap_by_id>](&swap.id())
4777 .unwrap()
4778 .ok_or(anyhow!("Could not retrieve {} swap", $type))
4779 .unwrap()
4780 }
4781 }};
4782 }
4783
4784 #[sdk_macros::async_test_all]
4785 async fn test_receive_swap_update_tracking() -> Result<()> {
4786 create_persister!(persister);
4787 let swapper = Arc::new(MockSwapper::default());
4788 let status_stream = Arc::new(MockStatusStream::new());
4789 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
4790 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
4791
4792 let sdk = new_liquid_sdk_with_chain_services(
4793 persister.clone(),
4794 swapper.clone(),
4795 status_stream.clone(),
4796 liquid_chain_service.clone(),
4797 bitcoin_chain_service.clone(),
4798 None,
4799 )
4800 .await?;
4801
4802 LiquidSdk::track_swap_updates(&sdk);
4803
4804 tokio::spawn(async move {
4806 let unrecoverable_states: [RevSwapStates; 4] = [
4808 RevSwapStates::SwapExpired,
4809 RevSwapStates::InvoiceExpired,
4810 RevSwapStates::TransactionFailed,
4811 RevSwapStates::TransactionRefunded,
4812 ];
4813
4814 for status in unrecoverable_states {
4815 let persisted_swap = trigger_swap_update!(
4816 "receive",
4817 NewSwapArgs::default(),
4818 persister,
4819 status_stream,
4820 status,
4821 None,
4822 None
4823 );
4824 assert_eq!(persisted_swap.state, PaymentState::Failed);
4825 }
4826
4827 for status in [
4830 RevSwapStates::TransactionMempool,
4831 RevSwapStates::TransactionConfirmed,
4832 ] {
4833 let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
4834 let mock_tx_id = mock_tx.txid();
4835 let height = (serde_json::to_string(&status).unwrap()
4836 == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
4837 as i32;
4838 liquid_chain_service.set_history(vec![LBtcHistory {
4839 txid: mock_tx_id,
4840 height,
4841 }]);
4842
4843 let persisted_swap = trigger_swap_update!(
4844 "receive",
4845 NewSwapArgs::default(),
4846 persister,
4847 status_stream,
4848 status,
4849 Some(TransactionInfo {
4850 id: mock_tx_id.to_string(),
4851 hex: Some(
4852 lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
4853 ),
4854 eta: None,
4855 }),
4856 None
4857 );
4858 assert!(persisted_swap.claim_tx_id.is_some());
4859 }
4860
4861 for status in [
4864 RevSwapStates::TransactionMempool,
4865 RevSwapStates::TransactionConfirmed,
4866 ] {
4867 let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
4868 let mock_tx_id = mock_tx.txid();
4869 let height = (serde_json::to_string(&status).unwrap()
4870 == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
4871 as i32;
4872 liquid_chain_service.set_history(vec![LBtcHistory {
4873 txid: mock_tx_id,
4874 height,
4875 }]);
4876
4877 let persisted_swap = trigger_swap_update!(
4878 "receive",
4879 NewSwapArgs::default().set_receiver_amount_sat(Some(1000)),
4880 persister,
4881 status_stream,
4882 status,
4883 Some(TransactionInfo {
4884 id: mock_tx_id.to_string(),
4885 hex: Some(
4886 lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
4887 ),
4888 eta: None
4889 }),
4890 None
4891 );
4892 assert!(persisted_swap.claim_tx_id.is_none());
4893 }
4894 })
4895 .await
4896 .unwrap();
4897
4898 Ok(())
4899 }
4900
4901 #[sdk_macros::async_test_all]
4902 async fn test_send_swap_update_tracking() -> Result<()> {
4903 create_persister!(persister);
4904 let swapper = Arc::new(MockSwapper::default());
4905 let status_stream = Arc::new(MockStatusStream::new());
4906
4907 let sdk = Arc::new(
4908 new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?,
4909 );
4910
4911 LiquidSdk::track_swap_updates(&sdk);
4912
4913 tokio::spawn(async move {
4915 let unrecoverable_states: [SubSwapStates; 3] = [
4917 SubSwapStates::TransactionLockupFailed,
4918 SubSwapStates::InvoiceFailedToPay,
4919 SubSwapStates::SwapExpired,
4920 ];
4921
4922 for status in unrecoverable_states {
4923 let persisted_swap = trigger_swap_update!(
4924 "send",
4925 NewSwapArgs::default(),
4926 persister,
4927 status_stream,
4928 status,
4929 None,
4930 None
4931 );
4932 assert_eq!(persisted_swap.state, PaymentState::Failed);
4933 }
4934
4935 let persisted_swap = trigger_swap_update!(
4938 "send",
4939 NewSwapArgs::default(),
4940 persister,
4941 status_stream,
4942 SubSwapStates::TransactionClaimPending,
4943 None,
4944 None
4945 );
4946 assert_eq!(persisted_swap.state, PaymentState::Complete);
4947 assert!(persisted_swap.preimage.is_some());
4948 })
4949 .await
4950 .unwrap();
4951
4952 Ok(())
4953 }
4954
4955 #[sdk_macros::async_test_all]
4956 async fn test_chain_swap_update_tracking() -> Result<()> {
4957 create_persister!(persister);
4958 let swapper = Arc::new(MockSwapper::default());
4959 let status_stream = Arc::new(MockStatusStream::new());
4960 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
4961 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
4962
4963 let sdk = new_liquid_sdk_with_chain_services(
4964 persister.clone(),
4965 swapper.clone(),
4966 status_stream.clone(),
4967 liquid_chain_service.clone(),
4968 bitcoin_chain_service.clone(),
4969 None,
4970 )
4971 .await?;
4972
4973 LiquidSdk::track_swap_updates(&sdk);
4974
4975 tokio::spawn(async move {
4977 let trigger_failed: [ChainSwapStates; 3] = [
4978 ChainSwapStates::TransactionFailed,
4979 ChainSwapStates::SwapExpired,
4980 ChainSwapStates::TransactionRefunded,
4981 ];
4982
4983 for direction in [Direction::Incoming, Direction::Outgoing] {
4985 for status in &trigger_failed {
4987 let persisted_swap = trigger_swap_update!(
4988 "chain",
4989 NewSwapArgs::default().set_direction(direction),
4990 persister,
4991 status_stream,
4992 status,
4993 None,
4994 None
4995 );
4996 assert_eq!(persisted_swap.state, PaymentState::Failed);
4997 }
4998
4999 let (mock_user_lockup_tx_hex, mock_user_lockup_tx_id) = match direction {
5000 Direction::Outgoing => {
5001 let tx = TEST_LIQUID_OUTGOING_USER_LOCKUP_TX.clone();
5002 (
5003 lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5004 tx.txid().to_string(),
5005 )
5006 }
5007 Direction::Incoming => {
5008 let tx = TEST_BITCOIN_INCOMING_USER_LOCKUP_TX.clone();
5009 (
5010 sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5011 tx.txid().to_string(),
5012 )
5013 }
5014 };
5015
5016 let (mock_server_lockup_tx_hex, mock_server_lockup_tx_id) = match direction {
5017 Direction::Incoming => {
5018 let tx = TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX.clone();
5019 (
5020 lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5021 tx.txid().to_string(),
5022 )
5023 }
5024 Direction::Outgoing => {
5025 let tx = TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX.clone();
5026 (
5027 sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5028 tx.txid().to_string(),
5029 )
5030 }
5031 };
5032
5033 for user_lockup_tx_id in &[None, Some(mock_user_lockup_tx_id.clone())] {
5037 if let Some(user_lockup_tx_id) = user_lockup_tx_id {
5038 match direction {
5039 Direction::Incoming => {
5040 bitcoin_chain_service.set_history(vec![BtcHistory {
5041 txid: bitcoin::Txid::from_str(user_lockup_tx_id).unwrap(),
5042 height: 0,
5043 }]);
5044 }
5045 Direction::Outgoing => {
5046 liquid_chain_service.set_history(vec![LBtcHistory {
5047 txid: elements::Txid::from_str(user_lockup_tx_id).unwrap(),
5048 height: 0,
5049 }]);
5050 }
5051 }
5052 }
5053 let persisted_swap = trigger_swap_update!(
5054 "chain",
5055 NewSwapArgs::default()
5056 .set_direction(direction)
5057 .set_initial_payment_state(PaymentState::Pending)
5058 .set_user_lockup_tx_id(user_lockup_tx_id.clone()),
5059 persister,
5060 status_stream,
5061 ChainSwapStates::TransactionLockupFailed,
5062 None,
5063 None
5064 );
5065 let expected_state = if user_lockup_tx_id.is_some() {
5066 match direction {
5067 Direction::Incoming => PaymentState::Refundable,
5068 Direction::Outgoing => PaymentState::RefundPending,
5069 }
5070 } else {
5071 PaymentState::Failed
5072 };
5073 assert_eq!(persisted_swap.state, expected_state);
5074 }
5075
5076 for status in [
5079 ChainSwapStates::TransactionMempool,
5080 ChainSwapStates::TransactionConfirmed,
5081 ] {
5082 if direction == Direction::Incoming {
5083 bitcoin_chain_service.set_history(vec![BtcHistory {
5084 txid: bitcoin::Txid::from_str(&mock_user_lockup_tx_id).unwrap(),
5085 height: 0,
5086 }]);
5087 bitcoin_chain_service.set_transactions(&[&mock_user_lockup_tx_hex]);
5088 }
5089 let persisted_swap = trigger_swap_update!(
5090 "chain",
5091 NewSwapArgs::default().set_direction(direction),
5092 persister,
5093 status_stream,
5094 status,
5095 Some(TransactionInfo {
5096 id: mock_user_lockup_tx_id.clone(),
5097 hex: Some(mock_user_lockup_tx_hex.clone()),
5098 eta: None
5099 }), Some(true) );
5102 assert_eq!(
5103 persisted_swap.user_lockup_tx_id,
5104 Some(mock_user_lockup_tx_id.clone())
5105 );
5106 assert!(!persisted_swap.accept_zero_conf);
5107 }
5108
5109 for accepts_zero_conf in [false, true] {
5115 let persisted_swap = trigger_swap_update!(
5116 "chain",
5117 NewSwapArgs::default()
5118 .set_direction(direction)
5119 .set_accepts_zero_conf(accepts_zero_conf)
5120 .set_set_actual_payer_amount(true),
5121 persister,
5122 status_stream,
5123 ChainSwapStates::TransactionServerMempool,
5124 Some(TransactionInfo {
5125 id: mock_server_lockup_tx_id.clone(),
5126 hex: Some(mock_server_lockup_tx_hex.clone()),
5127 eta: None,
5128 }),
5129 None
5130 );
5131 match accepts_zero_conf {
5132 false => {
5133 assert_eq!(persisted_swap.state, PaymentState::Pending);
5134 assert!(persisted_swap.server_lockup_tx_id.is_some());
5135 }
5136 true => {
5137 assert_eq!(persisted_swap.state, PaymentState::Pending);
5138 assert!(persisted_swap.claim_tx_id.is_some());
5139 }
5140 };
5141 }
5142
5143 let persisted_swap = trigger_swap_update!(
5146 "chain",
5147 NewSwapArgs::default()
5148 .set_direction(direction)
5149 .set_set_actual_payer_amount(true),
5150 persister,
5151 status_stream,
5152 ChainSwapStates::TransactionServerConfirmed,
5153 Some(TransactionInfo {
5154 id: mock_server_lockup_tx_id,
5155 hex: Some(mock_server_lockup_tx_hex),
5156 eta: None,
5157 }),
5158 None
5159 );
5160 assert_eq!(persisted_swap.state, PaymentState::Pending);
5161 assert!(persisted_swap.claim_tx_id.is_some());
5162 }
5163
5164 let persisted_swap = trigger_swap_update!(
5167 "chain",
5168 NewSwapArgs::default().set_direction(Direction::Outgoing),
5169 persister,
5170 status_stream,
5171 ChainSwapStates::Created,
5172 None,
5173 None
5174 );
5175 assert_eq!(persisted_swap.state, PaymentState::Pending);
5176 assert!(persisted_swap.user_lockup_tx_id.is_some());
5177 })
5178 .await
5179 .unwrap();
5180
5181 Ok(())
5182 }
5183
5184 #[sdk_macros::async_test_all]
5185 async fn test_zero_amount_chain_swap_zero_leeway() -> Result<()> {
5186 let user_lockup_sat = 50_000;
5187
5188 create_persister!(persister);
5189 let swapper = Arc::new(MockSwapper::new());
5190 let status_stream = Arc::new(MockStatusStream::new());
5191 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5192 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5193
5194 let sdk = new_liquid_sdk_with_chain_services(
5195 persister.clone(),
5196 swapper.clone(),
5197 status_stream.clone(),
5198 liquid_chain_service.clone(),
5199 bitcoin_chain_service.clone(),
5200 Some(0),
5201 )
5202 .await?;
5203
5204 LiquidSdk::track_swap_updates(&sdk);
5205
5206 tokio::spawn(async move {
5208 for fee_increase in [0, 1] {
5212 swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5213 user_lockup_sat,
5214 onchain_fee_increase_sat: fee_increase,
5215 });
5216 bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5217 let persisted_swap = trigger_swap_update!(
5218 "chain",
5219 NewSwapArgs::default()
5220 .set_direction(Direction::Incoming)
5221 .set_accepts_zero_conf(false)
5222 .set_zero_amount(true),
5223 persister,
5224 status_stream,
5225 ChainSwapStates::TransactionLockupFailed,
5226 None,
5227 None
5228 );
5229 match fee_increase {
5230 0 => {
5231 assert_eq!(persisted_swap.state, PaymentState::Created);
5232 }
5233 1 => {
5234 assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5235 }
5236 _ => panic!("Unexpected fee_increase"),
5237 }
5238 }
5239 })
5240 .await?;
5241
5242 Ok(())
5243 }
5244
5245 #[sdk_macros::async_test_all]
5246 async fn test_zero_amount_chain_swap_with_leeway() -> Result<()> {
5247 let user_lockup_sat = 50_000;
5248 let onchain_fee_rate_leeway_sat = 500;
5249
5250 create_persister!(persister);
5251 let swapper = Arc::new(MockSwapper::new());
5252 let status_stream = Arc::new(MockStatusStream::new());
5253 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5254 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5255
5256 let sdk = new_liquid_sdk_with_chain_services(
5257 persister.clone(),
5258 swapper.clone(),
5259 status_stream.clone(),
5260 liquid_chain_service.clone(),
5261 bitcoin_chain_service.clone(),
5262 Some(onchain_fee_rate_leeway_sat),
5263 )
5264 .await?;
5265
5266 LiquidSdk::track_swap_updates(&sdk);
5267
5268 tokio::spawn(async move {
5270 for fee_increase in [onchain_fee_rate_leeway_sat, onchain_fee_rate_leeway_sat + 1] {
5274 swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5275 user_lockup_sat,
5276 onchain_fee_increase_sat: fee_increase,
5277 });
5278 bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5279 let persisted_swap = trigger_swap_update!(
5280 "chain",
5281 NewSwapArgs::default()
5282 .set_direction(Direction::Incoming)
5283 .set_accepts_zero_conf(false)
5284 .set_zero_amount(true),
5285 persister,
5286 status_stream,
5287 ChainSwapStates::TransactionLockupFailed,
5288 None,
5289 None
5290 );
5291 match fee_increase {
5292 val if val == onchain_fee_rate_leeway_sat => {
5293 assert_eq!(persisted_swap.state, PaymentState::Created);
5294 }
5295 val if val == (onchain_fee_rate_leeway_sat + 1) => {
5296 assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5297 }
5298 _ => panic!("Unexpected fee_increase"),
5299 }
5300 }
5301 })
5302 .await?;
5303
5304 Ok(())
5305 }
5306
5307 #[sdk_macros::async_test_all]
5308 async fn test_background_tasks() -> Result<()> {
5309 create_persister!(persister);
5310 let swapper = Arc::new(MockSwapper::new());
5311 let status_stream = Arc::new(MockStatusStream::new());
5312 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5313 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5314
5315 let sdk = new_liquid_sdk_with_chain_services(
5316 persister.clone(),
5317 swapper.clone(),
5318 status_stream.clone(),
5319 liquid_chain_service.clone(),
5320 bitcoin_chain_service.clone(),
5321 None,
5322 )
5323 .await?;
5324
5325 sdk.start().await?;
5326
5327 tokio::time::sleep(Duration::from_secs(3)).await;
5328
5329 sdk.disconnect().await?;
5330
5331 Ok(())
5332 }
5333
5334 #[sdk_macros::async_test_all]
5335 async fn test_create_bolt12_offer() -> Result<()> {
5336 create_persister!(persister);
5337
5338 let swapper = Arc::new(MockSwapper::default());
5339 let status_stream = Arc::new(MockStatusStream::new());
5340 let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5341
5342 let webhook_url = "https://example.com/webhook";
5344 persister.set_webhook_url(webhook_url.to_string())?;
5345
5346 let description = "test offer".to_string();
5348 let response = sdk.create_bolt12_offer(description.clone()).await?;
5349
5350 assert!(!response.destination.is_empty());
5352
5353 let offers = persister.list_bolt12_offers_by_webhook_url(webhook_url)?;
5355 assert_eq!(offers.len(), 1);
5356
5357 let offer = &offers[0];
5359 assert_eq!(offer.description, description);
5360 assert_eq!(offer.webhook_url, Some(webhook_url.to_string()));
5361 assert_eq!(offer.id, response.destination);
5362
5363 assert!(!offer.private_key.is_empty());
5365
5366 Ok(())
5367 }
5368
5369 #[sdk_macros::async_test_all]
5370 async fn test_create_bolt12_receive_swap() -> Result<()> {
5371 create_persister!(persister);
5372
5373 let swapper = Arc::new(MockSwapper::default());
5374 let status_stream = Arc::new(MockStatusStream::new());
5375 let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5376
5377 let webhook_url = "https://example.com/webhook";
5379 persister.set_webhook_url(webhook_url.to_string())?;
5380
5381 let description = "test offer".to_string();
5383 let response = sdk.create_bolt12_offer(description.clone()).await?;
5384 let offer = persister
5385 .fetch_bolt12_offer_by_id(&response.destination)?
5386 .unwrap();
5387
5388 let expanded_key = ExpandedKey::new([42; 32]);
5390 let entropy_source = RandomBytes::new(utils::generate_entropy());
5391 let nonce = Nonce::from_entropy_source(&entropy_source);
5392 let secp = Secp256k1::new();
5393 let payment_id = PaymentId([1; 32]);
5394 let invoice_request = TryInto::<Offer>::try_into(offer.clone())?
5395 .request_invoice(&expanded_key, nonce, &secp, payment_id)
5396 .unwrap()
5397 .amount_msats(1_000_000)
5398 .unwrap()
5399 .chain(Network::Testnet)
5400 .unwrap()
5401 .build_and_sign()
5402 .unwrap();
5403 let mut buffer = Vec::new();
5404 invoice_request.write(&mut buffer).unwrap();
5405
5406 let create_res = sdk
5408 .create_bolt12_invoice(&CreateBolt12InvoiceRequest {
5409 offer: offer.id,
5410 invoice_request: buffer.to_hex(),
5411 })
5412 .await
5413 .unwrap();
5414 assert!(create_res.invoice.starts_with("lni"));
5415
5416 Ok(())
5417 }
5418}