1use base64::Engine;
2use bitcoin::{
3 consensus::serialize,
4 hashes::{Hash, sha256},
5 hex::DisplayHex,
6};
7use breez_sdk_common::input::InputType;
8pub use breez_sdk_common::input::parse as parse_input;
9use breez_sdk_common::{
10 lnurl::{
11 error::LnurlError,
12 pay::{
13 AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed,
14 ValidatedCallbackResponse, validate_lnurl_pay,
15 },
16 },
17 rest::RestClient,
18};
19use spark_wallet::{
20 DefaultSigner, ExitSpeed, Order, PagingFilter, PayLightningInvoiceResult, SparkAddress,
21 SparkWallet, WalletEvent, WalletTransfer,
22};
23use std::{str::FromStr, sync::Arc};
24use tracing::{error, info, trace};
25use web_time::{Duration, SystemTime};
26
27use tokio::{select, sync::watch};
28use tokio_with_wasm::alias as tokio;
29use web_time::Instant;
30use x509_parser::parse_x509_certificate;
31
32use crate::{
33 BitcoinChainService, ClaimDepositRequest, ClaimDepositResponse, DepositInfo, Fee,
34 GetPaymentRequest, GetPaymentResponse, ListUnclaimedDepositsRequest,
35 ListUnclaimedDepositsResponse, LnurlPayInfo, LnurlPayRequest, LnurlPayResponse, Logger,
36 Network, PaymentDetails, PaymentStatus, PrepareLnurlPayRequest, PrepareLnurlPayResponse,
37 RefundDepositRequest, RefundDepositResponse, SendPaymentOptions,
38 error::SdkError,
39 events::{EventEmitter, EventListener, SdkEvent},
40 logger,
41 models::{
42 Config, GetInfoRequest, GetInfoResponse, ListPaymentsRequest, ListPaymentsResponse,
43 Payment, PrepareSendPaymentRequest, PrepareSendPaymentResponse, ReceivePaymentMethod,
44 ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentMethod, SendPaymentRequest,
45 SendPaymentResponse, SyncWalletRequest, SyncWalletResponse,
46 },
47 persist::{
48 CachedAccountInfo, CachedSyncInfo, ObjectCacheRepository, PaymentMetadata,
49 StaticDepositAddress, Storage, UpdateDepositPayload,
50 },
51 utils::{
52 deposit_chain_syncer::DepositChainSyncer,
53 utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo},
54 },
55};
56
57#[derive(Clone, Debug)]
58enum SyncType {
59 Full,
60 PaymentsOnly,
61}
62
63#[derive(Clone)]
66#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
67pub struct BreezSdk {
68 config: Config,
69 spark_wallet: Arc<SparkWallet<DefaultSigner>>,
70 storage: Arc<dyn Storage>,
71 chain_service: Arc<dyn BitcoinChainService>,
72 lnurl_client: Arc<dyn RestClient>,
73 event_emitter: Arc<EventEmitter>,
74 shutdown_sender: watch::Sender<()>,
75 shutdown_receiver: watch::Receiver<()>,
76 sync_trigger: tokio::sync::broadcast::Sender<SyncType>,
77}
78
79#[cfg_attr(feature = "uniffi", uniffi::export)]
80pub fn init_logging(
81 log_dir: Option<String>,
82 app_logger: Option<Box<dyn Logger>>,
83 log_filter: Option<String>,
84) -> Result<(), SdkError> {
85 logger::init_logging(log_dir, app_logger, log_filter)
86}
87
88#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
89#[cfg_attr(feature = "uniffi", uniffi::export)]
90#[allow(clippy::needless_pass_by_value)]
91pub fn default_storage(data_dir: String) -> Result<Arc<dyn Storage>, SdkError> {
92 let db_path = std::path::PathBuf::from_str(&data_dir)?;
93
94 let storage = crate::SqliteStorage::new(&db_path)?;
95 Ok(Arc::new(storage))
96}
97
98#[cfg_attr(feature = "uniffi", uniffi::export)]
99pub fn default_config(network: Network) -> Config {
100 Config {
101 api_key: None,
102 network,
103 sync_interval_secs: 60, max_deposit_claim_fee: None,
105 }
106}
107
108#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
109pub async fn parse(input: &str) -> Result<InputType, SdkError> {
110 Ok(parse_input(input).await?)
111}
112
113impl BreezSdk {
114 pub(crate) async fn new(
129 config: Config,
130 signer: DefaultSigner,
131 storage: Arc<dyn Storage>,
132 chain_service: Arc<dyn BitcoinChainService>,
133 lnurl_client: Arc<dyn RestClient>,
134 shutdown_sender: watch::Sender<()>,
135 shutdown_receiver: watch::Receiver<()>,
136 ) -> Result<Self, SdkError> {
137 let spark_wallet_config =
138 spark_wallet::SparkWalletConfig::default_config(config.clone().network.into());
139 let spark_wallet = SparkWallet::connect(spark_wallet_config, signer).await?;
140
141 match &config.api_key {
142 Some(api_key) => validate_breez_api_key(api_key)?,
143 None => return Err(SdkError::Generic("Missing Breez API key".to_string())),
144 }
145 let sdk = Self {
146 config,
147 spark_wallet: Arc::new(spark_wallet),
148 storage,
149 chain_service,
150 lnurl_client,
151 event_emitter: Arc::new(EventEmitter::new()),
152 shutdown_sender,
153 shutdown_receiver,
154 sync_trigger: tokio::sync::broadcast::channel(10).0,
155 };
156 Ok(sdk)
157 }
158
159 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
169 pub async fn connect(request: crate::ConnectRequest) -> Result<Self, SdkError> {
170 let db_path = std::path::PathBuf::from_str(&request.storage_dir)?;
171 let path_suffix: String = sha256::Hash::hash(request.mnemonic.as_bytes())
172 .to_string()
173 .chars()
174 .take(8)
175 .collect();
176 let storage_dir = db_path
177 .join(request.config.network.to_string().to_lowercase())
178 .join(path_suffix);
179
180 let storage = default_storage(storage_dir.to_string_lossy().to_string())?;
181 let builder = crate::SdkBuilder::new(request.config, request.mnemonic, storage);
182 builder.build().await
183 }
184
185 pub(crate) fn start(&self) {
191 self.periodic_sync();
192 }
193
194 fn periodic_sync(&self) {
195 let sdk = self.clone();
196 let mut shutdown_receiver = sdk.shutdown_receiver.clone();
197 let mut subscription = sdk.spark_wallet.subscribe_events();
198 let sync_trigger_sender = sdk.sync_trigger.clone();
199 let mut sync_trigger_receiver = sdk.sync_trigger.clone().subscribe();
200 let mut last_sync_time = SystemTime::now();
201 let sync_interval = u64::from(self.config.sync_interval_secs);
202 tokio::spawn(async move {
203 loop {
204 tokio::select! {
205 _ = shutdown_receiver.changed() => {
206 info!("Deposit tracking loop shutdown signal received");
207 return;
208 }
209 event = subscription.recv() => {
210 match event {
211 Ok(event) => {
212 info!("Received event: {event}");
213 trace!("Received event: {:?}", event);
214 sdk.handle_wallet_event(event);
215 }
216 Err(e) => {
217 error!("Failed to receive event: {e:?}");
218 }
219 }
220 }
221 () = async {
222 let sync_type_res = sync_trigger_receiver.recv().await;
223 if let Ok(sync_type) = sync_type_res {
224 info!("Sync trigger changed: {:?}", &sync_type);
225
226 if let Err(e) = sdk.sync_wallet_internal(sync_type.clone()).await {
227 error!("Failed to sync wallet: {e:?}");
228 } else if matches!(sync_type, SyncType::Full) {
229 last_sync_time = SystemTime::now();
230 }
231 }
232 } => {}
233 () = tokio::time::sleep(Duration::from_secs(10)) => {
235 let now = SystemTime::now();
236 if let Ok(elapsed) = now.duration_since(last_sync_time) && elapsed.as_secs() >= sync_interval
237 && let Err(e) = sync_trigger_sender.send(SyncType::Full) {
238 error!("Failed to trigger periodic sync: {e:?}");
239 }
240 }
241 }
242 }
243 });
244 }
245
246 fn handle_wallet_event(&self, event: WalletEvent) {
247 match event {
248 WalletEvent::DepositConfirmed(_) => {
249 info!("Deposit confirmed");
250 }
251 WalletEvent::StreamConnected => {
252 info!("Stream connected");
253 }
254 WalletEvent::StreamDisconnected => {
255 info!("Stream disconnected");
256 }
257 WalletEvent::Synced => {
258 info!("Synced");
259 if let Err(e) = self.sync_trigger.send(SyncType::Full) {
260 error!("Failed to sync wallet: {e:?}");
261 }
262 }
263 WalletEvent::TransferClaimed(transfer) => {
264 info!("Transfer claimed");
265 if let Ok(payment) = transfer.try_into() {
266 self.event_emitter
267 .emit(&SdkEvent::PaymentSucceeded { payment });
268 }
269 if let Err(e) = self.sync_trigger.send(SyncType::PaymentsOnly) {
270 error!("Failed to sync wallet: {e:?}");
271 }
272 }
273 }
274 }
275
276 async fn sync_wallet_internal(&self, sync_type: SyncType) -> Result<(), SdkError> {
277 let start_time = Instant::now();
278 if let SyncType::Full = sync_type {
279 info!("sync_wallet_internal: Syncing with Spark network");
281 self.spark_wallet.sync().await?;
282 info!("sync_wallet_internal: Synced with Spark network completed");
283 }
284 self.sync_payments_to_storage().await?;
285 info!("sync_wallet_internal: Synced payments to storage completed");
286 self.check_and_claim_static_deposits().await?;
287 info!("sync_wallet_internal: Checked and claimed static deposits completed");
288 let elapsed = start_time.elapsed();
289 info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}");
290 self.event_emitter.emit(&SdkEvent::Synced {});
291 Ok(())
292 }
293
294 async fn sync_payments_to_storage(&self) -> Result<(), SdkError> {
296 const BATCH_SIZE: u64 = 50;
297
298 let balance = self.spark_wallet.get_balance().await?;
300 let object_repository = ObjectCacheRepository::new(self.storage.clone());
301 object_repository
302 .save_account_info(&CachedAccountInfo {
303 balance_sats: balance,
304 })
305 .await?;
306
307 let cached_sync_info = object_repository
309 .fetch_sync_info()
310 .await?
311 .unwrap_or_default();
312 let current_offset = cached_sync_info.offset;
313
314 let mut next_offset = current_offset;
316 let mut has_more = true;
317 info!("Syncing payments to storage, offset = {next_offset}");
318 let mut pending_payments: u64 = 0;
319 while has_more {
320 let transfers_response = self
322 .spark_wallet
323 .list_transfers(Some(PagingFilter::new(
324 Some(next_offset),
325 Some(BATCH_SIZE),
326 Some(Order::Ascending),
327 )))
328 .await?;
329
330 info!(
331 "Syncing payments to storage, offset = {next_offset}, transfers = {}",
332 transfers_response.len()
333 );
334 for transfer in &transfers_response {
336 let payment: Payment = transfer.clone().try_into()?;
338 if let Err(err) = self.storage.insert_payment(payment.clone()).await {
340 error!("Failed to insert payment: {err:?}");
341 }
342 if payment.status == PaymentStatus::Pending {
343 pending_payments = pending_payments.saturating_add(1);
344 }
345 info!("Inserted payment: {payment:?}");
346 }
347
348 next_offset = next_offset.saturating_add(u64::try_from(transfers_response.len())?);
350 let save_res = object_repository
353 .save_sync_info(&CachedSyncInfo {
354 offset: next_offset.saturating_sub(pending_payments),
355 })
356 .await;
357
358 if let Err(err) = save_res {
359 error!("Failed to update last sync offset: {err:?}");
360 }
361 has_more = transfers_response.len() as u64 == BATCH_SIZE;
362 }
363
364 Ok(())
365 }
366
367 async fn check_and_claim_static_deposits(&self) -> Result<(), SdkError> {
368 let to_claim = DepositChainSyncer::new(
369 self.chain_service.clone(),
370 self.storage.clone(),
371 self.spark_wallet.clone(),
372 )
373 .sync()
374 .await?;
375
376 let mut claimed_deposits: Vec<DepositInfo> = Vec::new();
377 let mut unclaimed_deposits: Vec<DepositInfo> = Vec::new();
378 for detailed_utxo in to_claim {
379 match self
380 .claim_utxo(&detailed_utxo, self.config.max_deposit_claim_fee.clone())
381 .await
382 {
383 Ok(_) => {
384 info!("Claimed utxo {}:{}", detailed_utxo.txid, detailed_utxo.vout);
385 self.storage
386 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
387 .await?;
388 claimed_deposits.push(detailed_utxo.into());
389 }
390 Err(e) => {
391 error!(
392 "Failed to claim utxo {}:{}: {e}",
393 detailed_utxo.txid, detailed_utxo.vout
394 );
395 self.storage
396 .update_deposit(
397 detailed_utxo.txid.to_string(),
398 detailed_utxo.vout,
399 UpdateDepositPayload::ClaimError {
400 error: e.clone().into(),
401 },
402 )
403 .await?;
404 let mut unclaimed_deposit: DepositInfo = detailed_utxo.clone().into();
405 unclaimed_deposit.claim_error = Some(e.into());
406 unclaimed_deposits.push(unclaimed_deposit);
407 }
408 }
409 }
410
411 info!("background claim completed, unclaimed deposits: {unclaimed_deposits:?}");
412
413 if !unclaimed_deposits.is_empty() {
414 self.event_emitter
415 .emit(&SdkEvent::ClaimDepositsFailed { unclaimed_deposits });
416 }
417 if !claimed_deposits.is_empty() {
418 self.event_emitter
419 .emit(&SdkEvent::ClaimDepositsSucceeded { claimed_deposits });
420 }
421 Ok(())
422 }
423
424 async fn claim_utxo(
425 &self,
426 detailed_utxo: &DetailedUtxo,
427 max_claim_fee: Option<Fee>,
428 ) -> Result<WalletTransfer, SdkError> {
429 info!(
430 "Fetching static deposit claim quote for deposit tx {}:{} and amount: {}",
431 detailed_utxo.txid, detailed_utxo.vout, detailed_utxo.value
432 );
433
434 let quote = self
435 .spark_wallet
436 .fetch_static_deposit_claim_quote(detailed_utxo.tx.clone(), Some(detailed_utxo.vout))
437 .await?;
438 let spark_requested_fee = detailed_utxo.value.saturating_sub(quote.credit_amount_sats);
439 if let Some(max_deposit_claim_fee) = max_claim_fee {
440 match max_deposit_claim_fee {
441 Fee::Fixed { amount } => {
442 info!(
443 "User max fee: {} spark requested fee: {}",
444 amount, spark_requested_fee
445 );
446 if spark_requested_fee > amount {
447 return Err(SdkError::DepositClaimFeeExceeded {
448 tx: detailed_utxo.txid.to_string(),
449 vout: detailed_utxo.vout,
450 max_fee: max_deposit_claim_fee,
451 actual_fee: spark_requested_fee,
452 });
453 }
454 }
455 Fee::Rate { sat_per_vbyte } => {
456 const CLAIM_TX_SIZE: u64 = 99;
458 let user_max_fee = CLAIM_TX_SIZE.saturating_mul(sat_per_vbyte);
459 info!(
460 "User max fee: {} spark requested fee: {}",
461 user_max_fee, spark_requested_fee
462 );
463 if spark_requested_fee > user_max_fee {
464 return Err(SdkError::DepositClaimFeeExceeded {
465 tx: detailed_utxo.txid.to_string(),
466 vout: detailed_utxo.vout,
467 max_fee: max_deposit_claim_fee,
468 actual_fee: spark_requested_fee,
469 });
470 }
471 }
472 }
473 }
474 info!(
475 "Claiming static deposit for utxo {}:{}",
476 detailed_utxo.txid, detailed_utxo.vout
477 );
478 let transfer = self.spark_wallet.claim_static_deposit(quote).await?;
479 info!(
480 "Claimed static deposit transfer: {}",
481 serde_json::to_string_pretty(&transfer)?
482 );
483 Ok(transfer)
484 }
485}
486
487#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
488#[allow(clippy::needless_pass_by_value)]
489impl BreezSdk {
490 pub fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
500 self.event_emitter.add_listener(listener)
501 }
502
503 pub fn remove_event_listener(&self, id: &str) -> bool {
513 self.event_emitter.remove_listener(id)
514 }
515
516 pub fn disconnect(&self) -> Result<(), SdkError> {
525 self.shutdown_sender
526 .send(())
527 .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
528
529 Ok(())
530 }
531
532 #[allow(unused_variables)]
534 pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
535 let object_repository = ObjectCacheRepository::new(self.storage.clone());
536 let account_info = object_repository
537 .fetch_account_info()
538 .await?
539 .unwrap_or_default();
540 Ok(GetInfoResponse {
541 balance_sats: account_info.balance_sats,
542 })
543 }
544
545 pub async fn receive_payment(
546 &self,
547 request: ReceivePaymentRequest,
548 ) -> Result<ReceivePaymentResponse, SdkError> {
549 match &request.payment_method {
550 ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse {
551 fee_sats: 0,
552 payment_request: self.spark_wallet.get_spark_address().await?.to_string(),
553 }),
554 ReceivePaymentMethod::BitcoinAddress => {
555 let object_repository = ObjectCacheRepository::new(self.storage.clone());
558
559 let static_deposit_address =
561 object_repository.fetch_static_deposit_address().await?;
562 if let Some(static_deposit_address) = static_deposit_address {
563 return Ok(ReceivePaymentResponse {
564 payment_request: static_deposit_address.address.to_string(),
565 fee_sats: 0,
566 });
567 }
568
569 let deposit_addresses = self
571 .spark_wallet
572 .list_static_deposit_addresses(None)
573 .await?;
574
575 let address = match deposit_addresses.last() {
577 Some(address) => address.to_string(),
578 None => self
579 .spark_wallet
580 .generate_deposit_address(true)
581 .await?
582 .to_string(),
583 };
584
585 object_repository
586 .save_static_deposit_address(&StaticDepositAddress {
587 address: address.clone(),
588 })
589 .await?;
590
591 Ok(ReceivePaymentResponse {
592 payment_request: address,
593 fee_sats: 0,
594 })
595 }
596 ReceivePaymentMethod::Bolt11Invoice {
597 description,
598 amount_sats,
599 } => Ok(ReceivePaymentResponse {
600 payment_request: self
601 .spark_wallet
602 .create_lightning_invoice(
603 amount_sats.unwrap_or_default(),
604 Some(description.clone()),
605 )
606 .await?
607 .invoice,
608 fee_sats: 0,
609 }),
610 }
611 }
612
613 pub async fn prepare_lnurl_pay(
614 &self,
615 request: PrepareLnurlPayRequest,
616 ) -> Result<PrepareLnurlPayResponse, SdkError> {
617 let success_data = match validate_lnurl_pay(
618 self.lnurl_client.as_ref(),
619 request.amount_sats.saturating_mul(1_000),
620 &None,
621 &request.pay_request,
622 self.config.network.into(),
623 request.validate_success_action_url,
624 )
625 .await?
626 {
627 ValidatedCallbackResponse::EndpointError { data } => {
628 return Err(LnurlError::EndpointError(data.reason).into());
629 }
630 ValidatedCallbackResponse::EndpointSuccess { data } => data,
631 };
632
633 let prepare_response = self
634 .prepare_send_payment(PrepareSendPaymentRequest {
635 payment_request: success_data.pr,
636 amount_sats: Some(request.amount_sats),
637 })
638 .await?;
639
640 let SendPaymentMethod::Bolt11Invoice {
641 invoice_details,
642 lightning_fee_sats,
643 ..
644 } = prepare_response.payment_method
645 else {
646 return Err(SdkError::Generic(
647 "Expected Bolt11Invoice payment method".to_string(),
648 ));
649 };
650
651 Ok(PrepareLnurlPayResponse {
652 amount_sats: request.amount_sats,
653 comment: request.comment,
654 pay_request: request.pay_request,
655 invoice_details,
656 fee_sats: lightning_fee_sats,
657 success_action: success_data.success_action,
658 })
659 }
660
661 pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
662 let mut payment = self
663 .send_payment_internal(
664 SendPaymentRequest {
665 prepare_response: PrepareSendPaymentResponse {
666 payment_method: SendPaymentMethod::Bolt11Invoice {
667 invoice_details: request.prepare_response.invoice_details,
668 spark_transfer_fee_sats: None,
669 lightning_fee_sats: request.prepare_response.fee_sats,
670 },
671 amount_sats: request.prepare_response.amount_sats,
672 },
673 options: None,
674 },
675 true,
676 )
677 .await?
678 .payment;
679
680 let success_action =
681 process_success_action(&payment, request.prepare_response.success_action.as_ref())?;
682
683 let lnurl_info = LnurlPayInfo {
684 ln_address: request.prepare_response.pay_request.address,
685 comment: request.prepare_response.comment,
686 domain: Some(request.prepare_response.pay_request.domain),
687 metadata: Some(request.prepare_response.pay_request.metadata_str),
688 processed_success_action: success_action.clone(),
689 raw_success_action: request.prepare_response.success_action,
690 };
691 let Some(PaymentDetails::Lightning { lnurl_pay_info, .. }) = &mut payment.details else {
692 return Err(SdkError::Generic(
693 "Expected Lightning payment details".to_string(),
694 ));
695 };
696 *lnurl_pay_info = Some(lnurl_info.clone());
697
698 self.storage
699 .set_payment_metadata(
700 payment.id.clone(),
701 PaymentMetadata {
702 lnurl_pay_info: Some(lnurl_info),
703 },
704 )
705 .await?;
706 self.event_emitter.emit(&SdkEvent::PaymentSucceeded {
707 payment: payment.clone(),
708 });
709 Ok(LnurlPayResponse {
710 payment,
711 success_action,
712 })
713 }
714
715 pub async fn prepare_send_payment(
716 &self,
717 request: PrepareSendPaymentRequest,
718 ) -> Result<PrepareSendPaymentResponse, SdkError> {
719 if let Ok(spark_address) = request.payment_request.parse::<SparkAddress>() {
721 return Ok(PrepareSendPaymentResponse {
722 payment_method: SendPaymentMethod::SparkAddress {
723 address: spark_address.to_string(),
724 fee_sats: 0,
725 },
726 amount_sats: request
727 .amount_sats
728 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
729 });
730 }
731 let parsed_input = parse(&request.payment_request).await?;
733 match &parsed_input {
734 InputType::Bolt11Invoice(detailed_bolt11_invoice) => {
735 let spark_address = self
736 .spark_wallet
737 .extract_spark_address(&request.payment_request)?;
738
739 let spark_transfer_fee_sats = if spark_address.is_some() {
740 Some(0)
741 } else {
742 None
743 };
744
745 let lightning_fee_sats = self
746 .spark_wallet
747 .fetch_lightning_send_fee_estimate(
748 &request.payment_request,
749 request.amount_sats,
750 )
751 .await?;
752
753 Ok(PrepareSendPaymentResponse {
754 payment_method: SendPaymentMethod::Bolt11Invoice {
755 invoice_details: detailed_bolt11_invoice.clone(),
756 spark_transfer_fee_sats,
757 lightning_fee_sats,
758 },
759 amount_sats: request
760 .amount_sats
761 .or(detailed_bolt11_invoice.amount_msat.map(|msat| msat / 1000))
762 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
763 })
764 }
765 InputType::BitcoinAddress(withdrawal_address) => {
766 let fee_quote = self
767 .spark_wallet
768 .fetch_coop_exit_fee_quote(
769 &withdrawal_address.address,
770 Some(
771 request
772 .amount_sats
773 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
774 ),
775 )
776 .await?;
777 Ok(PrepareSendPaymentResponse {
778 payment_method: SendPaymentMethod::BitcoinAddress {
779 address: withdrawal_address.clone(),
780 fee_quote: fee_quote.into(),
781 },
782 amount_sats: request
783 .amount_sats
784 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
785 })
786 }
787 _ => Err(SdkError::InvalidInput(
788 "Unsupported payment method".to_string(),
789 )),
790 }
791 }
792
793 pub async fn send_payment(
794 &self,
795 request: SendPaymentRequest,
796 ) -> Result<SendPaymentResponse, SdkError> {
797 self.send_payment_internal(request, false).await
798 }
799
800 async fn send_payment_internal(
801 &self,
802 request: SendPaymentRequest,
803 suppress_payment_event: bool,
804 ) -> Result<SendPaymentResponse, SdkError> {
805 let res = match request.prepare_response.payment_method {
806 SendPaymentMethod::SparkAddress { address, .. } => {
807 let spark_address = address
808 .parse::<SparkAddress>()
809 .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
810 let transfer = self
811 .spark_wallet
812 .transfer(request.prepare_response.amount_sats, &spark_address)
813 .await?;
814 Ok(SendPaymentResponse {
815 payment: transfer.try_into()?,
816 })
817 }
818 SendPaymentMethod::Bolt11Invoice {
819 invoice_details,
820 spark_transfer_fee_sats,
821 lightning_fee_sats,
822 } => {
823 let amount_to_send = match invoice_details.amount_msat {
824 Some(_) => None,
826 None => Some(request.prepare_response.amount_sats),
828 };
829 let use_spark = match request.options {
830 Some(SendPaymentOptions::Bolt11Invoice { use_spark }) => use_spark,
831 _ => false,
832 };
833 let fee_sats = match (use_spark, spark_transfer_fee_sats, lightning_fee_sats) {
834 (true, Some(fee), _) => fee,
835 _ => lightning_fee_sats,
836 };
837 if use_spark && spark_transfer_fee_sats.is_none() {
838 return Err(SdkError::InvalidInput(
839 "Cannot use spark to pay invoice as it doesn't contain a spark address"
840 .to_string(),
841 ));
842 }
843
844 let payment_response = self
845 .spark_wallet
846 .pay_lightning_invoice(
847 &invoice_details.invoice.bolt11,
848 amount_to_send,
849 Some(fee_sats),
850 use_spark,
851 )
852 .await?;
853 let payment = match payment_response {
854 PayLightningInvoiceResult::LightningPayment(payment) => {
855 self.poll_lightning_send_payment(&payment.id);
856 Payment::from_lightning(payment, request.prepare_response.amount_sats)?
857 }
858 PayLightningInvoiceResult::Transfer(payment) => payment.try_into()?,
859 };
860 Ok(SendPaymentResponse { payment })
861 }
862 SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
863 let exit_speed: ExitSpeed = match request.options {
864 Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) => {
865 confirmation_speed.into()
866 }
867 None => ExitSpeed::Fast,
868 _ => {
869 return Err(SdkError::InvalidInput("Invalid options".to_string()));
870 }
871 };
872 let response = self
873 .spark_wallet
874 .withdraw(
875 &address.address,
876 Some(request.prepare_response.amount_sats),
877 exit_speed,
878 fee_quote.into(),
879 )
880 .await?;
881 Ok(SendPaymentResponse {
882 payment: response.try_into()?,
883 })
884 }
885 };
886 if let Ok(response) = &res {
887 if !suppress_payment_event {
891 self.event_emitter.emit(&SdkEvent::PaymentSucceeded {
892 payment: response.payment.clone(),
893 });
894 }
895 if let Err(e) = self.sync_trigger.send(SyncType::PaymentsOnly) {
896 error!("Failed to send sync trigger: {e:?}");
897 }
898 }
899 res
900 }
901
902 fn poll_lightning_send_payment(&self, payment_id: &str) {
904 const MAX_POLL_ATTEMPTS: u32 = 10;
905 info!("Polling lightning send payment {}", payment_id);
906
907 let spark_wallet = self.spark_wallet.clone();
908 let sync_trigger = self.sync_trigger.clone();
909 let payment_id = payment_id.to_string();
910 let mut shutdown = self.shutdown_receiver.clone();
911
912 tokio::spawn(async move {
913 for i in 0..MAX_POLL_ATTEMPTS {
914 info!(
915 "Polling lightning send payment {} attempt {}",
916 payment_id, i
917 );
918 select! {
919 _ = shutdown.changed() => {
920 info!("Shutdown signal received");
921 return;
922 },
923 p = spark_wallet.fetch_lightning_send_payment(&payment_id) => {
924 if let Ok(Some(p)) = p && p.payment_preimage.is_some(){
925 info!("Pollling payment preimage found");
926 if let Err(e) = sync_trigger.send(SyncType::PaymentsOnly) {
927 error!("Failed to send sync trigger: {e:?}");
928 }
929 return;
930 }
931 let sleep_time = if i < 5 {
932 Duration::from_secs(1)
933 } else {
934 Duration::from_secs(i.into())
935 };
936 tokio::time::sleep(sleep_time).await;
937 }
938 }
939 }
940 });
941 }
942
943 #[allow(unused_variables)]
945 pub fn sync_wallet(&self, request: SyncWalletRequest) -> Result<SyncWalletResponse, SdkError> {
946 if let Err(e) = self.sync_trigger.send(SyncType::Full) {
947 error!("Failed to send sync trigger: {e:?}");
948 }
949 Ok(SyncWalletResponse {})
950 }
951
952 pub async fn list_payments(
967 &self,
968 request: ListPaymentsRequest,
969 ) -> Result<ListPaymentsResponse, SdkError> {
970 let payments = self
971 .storage
972 .list_payments(request.offset, request.limit)
973 .await?;
974 Ok(ListPaymentsResponse { payments })
975 }
976
977 pub async fn get_payment(
978 &self,
979 request: GetPaymentRequest,
980 ) -> Result<GetPaymentResponse, SdkError> {
981 let payment = self.storage.get_payment_by_id(request.payment_id).await?;
982 Ok(GetPaymentResponse { payment })
983 }
984
985 pub async fn claim_deposit(
986 &self,
987 request: ClaimDepositRequest,
988 ) -> Result<ClaimDepositResponse, SdkError> {
989 let detailed_utxo =
990 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
991 .fetch_detailed_utxo(&request.txid, request.vout)
992 .await?;
993
994 let max_fee = request
995 .max_fee
996 .or(self.config.max_deposit_claim_fee.clone());
997 match self.claim_utxo(&detailed_utxo, max_fee).await {
998 Ok(transfer) => {
999 self.storage
1000 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
1001 .await?;
1002 if let Err(e) = self.sync_trigger.send(SyncType::PaymentsOnly) {
1003 error!("Failed to execute sync after deposit claim: {e:?}");
1004 }
1005 Ok(ClaimDepositResponse {
1006 payment: transfer.try_into()?,
1007 })
1008 }
1009 Err(e) => {
1010 error!("Failed to claim deposit: {e:?}");
1011 self.storage
1012 .update_deposit(
1013 detailed_utxo.txid.to_string(),
1014 detailed_utxo.vout,
1015 UpdateDepositPayload::ClaimError {
1016 error: e.clone().into(),
1017 },
1018 )
1019 .await?;
1020 Err(e)
1021 }
1022 }
1023 }
1024
1025 pub async fn refund_deposit(
1026 &self,
1027 request: RefundDepositRequest,
1028 ) -> Result<RefundDepositResponse, SdkError> {
1029 let detailed_utxo =
1030 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1031 .fetch_detailed_utxo(&request.txid, request.vout)
1032 .await?;
1033 let tx = self
1034 .spark_wallet
1035 .refund_static_deposit(
1036 detailed_utxo.clone().tx,
1037 Some(detailed_utxo.vout),
1038 &request.destination_address,
1039 request.fee.into(),
1040 )
1041 .await?;
1042 let deposit: DepositInfo = detailed_utxo.into();
1043 let tx_hex = serialize(&tx).as_hex().to_string();
1044 let tx_id = tx.compute_txid().to_string();
1045
1046 self.storage
1048 .update_deposit(
1049 deposit.txid.clone(),
1050 deposit.vout,
1051 UpdateDepositPayload::Refund {
1052 refund_tx: tx_hex.clone(),
1053 refund_txid: tx_id.clone(),
1054 },
1055 )
1056 .await?;
1057
1058 self.chain_service
1059 .broadcast_transaction(tx_hex.clone())
1060 .await?;
1061 Ok(RefundDepositResponse { tx_id, tx_hex })
1062 }
1063
1064 #[allow(unused_variables)]
1065 pub async fn list_unclaimed_deposits(
1066 &self,
1067 request: ListUnclaimedDepositsRequest,
1068 ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
1069 let deposits = self.storage.list_deposits().await?;
1070 Ok(ListUnclaimedDepositsResponse { deposits })
1071 }
1072}
1073
1074fn process_success_action(
1075 payment: &Payment,
1076 success_action: Option<&SuccessAction>,
1077) -> Result<Option<SuccessActionProcessed>, LnurlError> {
1078 let Some(success_action) = success_action else {
1079 return Ok(None);
1080 };
1081
1082 let data = match success_action {
1083 SuccessAction::Aes { data } => data,
1084 SuccessAction::Message { data } => {
1085 return Ok(Some(SuccessActionProcessed::Message { data: data.clone() }));
1086 }
1087 SuccessAction::Url { data } => {
1088 return Ok(Some(SuccessActionProcessed::Url { data: data.clone() }));
1089 }
1090 };
1091
1092 let Some(PaymentDetails::Lightning { preimage, .. }) = &payment.details else {
1093 return Err(LnurlError::general(format!(
1094 "Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.",
1095 payment.details
1096 )));
1097 };
1098
1099 let Some(preimage) = preimage else {
1100 return Ok(None);
1101 };
1102
1103 let preimage =
1104 sha256::Hash::from_str(preimage).map_err(|_| LnurlError::general("Invalid preimage"))?;
1105 let preimage = preimage.as_byte_array();
1106 let result: AesSuccessActionDataResult = match (data, preimage).try_into() {
1107 Ok(data) => AesSuccessActionDataResult::Decrypted { data },
1108 Err(e) => AesSuccessActionDataResult::ErrorStatus {
1109 reason: e.to_string(),
1110 },
1111 };
1112
1113 Ok(Some(SuccessActionProcessed::Aes { result }))
1114}
1115
1116fn validate_breez_api_key(api_key: &str) -> Result<(), SdkError> {
1117 let api_key_decoded = base64::engine::general_purpose::STANDARD
1118 .decode(api_key.as_bytes())
1119 .map_err(|err| {
1120 SdkError::Generic(format!(
1121 "Could not base64 decode the Breez API key: {err:?}"
1122 ))
1123 })?;
1124 let (_rem, cert) = parse_x509_certificate(&api_key_decoded).map_err(|err| {
1125 SdkError::Generic(format!("Invaid certificate for Breez API key: {err:?}"))
1126 })?;
1127
1128 let issuer = cert
1129 .issuer()
1130 .iter_common_name()
1131 .next()
1132 .and_then(|cn| cn.as_str().ok());
1133 match issuer {
1134 Some(common_name) => {
1135 if !common_name.starts_with("Breez") {
1136 return Err(SdkError::Generic(
1137 "Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted"
1138 .to_string()
1139 ));
1140 }
1141 }
1142 _ => {
1143 return Err(SdkError::Generic(
1144 "Could not parse Breez API key certificate: issuer is invalid or not found."
1145 .to_string(),
1146 ));
1147 }
1148 }
1149
1150 Ok(())
1151}