1use base64::Engine;
2use bitcoin::{
3 consensus::serialize,
4 hashes::{Hash, sha256},
5 hex::DisplayHex,
6 secp256k1::{PublicKey, ecdsa::Signature},
7};
8use bitflags::bitflags;
9use breez_sdk_common::{
10 fiat::FiatService,
11 lnurl::{self, withdraw::execute_lnurl_withdraw},
12};
13use breez_sdk_common::{
14 lnurl::{
15 error::LnurlError,
16 pay::{
17 AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed, validate_lnurl_pay,
18 },
19 },
20 rest::RestClient,
21};
22use lnurl_models::sanitize_username;
23use spark_wallet::{
24 ExitSpeed, InvoiceDescription, Preimage, SparkAddress, SparkWallet, TransferId,
25 TransferTokenOutput, WalletEvent, WalletTransfer,
26};
27use std::{str::FromStr, sync::Arc};
28use tracing::{debug, error, info, trace, warn};
29use web_time::{Duration, SystemTime};
30
31use tokio::{
32 select,
33 sync::{Mutex, OnceCell, mpsc, oneshot, watch},
34 time::timeout,
35};
36use tokio_with_wasm::alias as tokio;
37use web_time::Instant;
38use x509_parser::parse_x509_certificate;
39
40use crate::{
41 AssetFilter, BitcoinAddressDetails, BitcoinChainService, Bolt11InvoiceDetails,
42 CheckLightningAddressRequest, CheckMessageRequest, CheckMessageResponse, ClaimDepositRequest,
43 ClaimDepositResponse, ClaimHtlcPaymentRequest, ClaimHtlcPaymentResponse, DepositInfo,
44 ExternalInputParser, GetPaymentRequest, GetPaymentResponse, GetTokensMetadataRequest,
45 GetTokensMetadataResponse, InputType, LightningAddressInfo, ListFiatCurrenciesResponse,
46 ListFiatRatesResponse, ListUnclaimedDepositsRequest, ListUnclaimedDepositsResponse,
47 LnurlPayInfo, LnurlPayRequest, LnurlPayResponse, LnurlWithdrawRequest, LnurlWithdrawResponse,
48 Logger, MaxFee, Network, PaymentDetails, PaymentStatus, PaymentType, PrepareLnurlPayRequest,
49 PrepareLnurlPayResponse, RefundDepositRequest, RefundDepositResponse,
50 RegisterLightningAddressRequest, SendOnchainFeeQuote, SendPaymentOptions, SetLnurlMetadataItem,
51 SignMessageRequest, SignMessageResponse, SparkHtlcOptions, UpdateUserSettingsRequest,
52 UserSettings, WaitForPaymentIdentifier,
53 chain::RecommendedFees,
54 error::SdkError,
55 events::{EventEmitter, EventListener, InternalSyncedEvent, SdkEvent},
56 issuer::TokenIssuer,
57 lnurl::{ListMetadataRequest, LnurlServerClient, PublishZapReceiptRequest},
58 logger,
59 models::{
60 Config, GetInfoRequest, GetInfoResponse, ListPaymentsRequest, ListPaymentsResponse,
61 Payment, PrepareSendPaymentRequest, PrepareSendPaymentResponse, ReceivePaymentMethod,
62 ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentMethod, SendPaymentRequest,
63 SendPaymentResponse, SyncWalletRequest, SyncWalletResponse,
64 },
65 nostr::NostrClient,
66 persist::{
67 CachedAccountInfo, ObjectCacheRepository, PaymentMetadata, PaymentRequestMetadata,
68 StaticDepositAddress, Storage, UpdateDepositPayload,
69 },
70 sync::SparkSyncService,
71 utils::{
72 deposit_chain_syncer::DepositChainSyncer,
73 run_with_shutdown,
74 send_payment_validation::validate_prepare_send_payment_request,
75 token::{get_tokens_metadata_cached_or_query, map_and_persist_token_transaction},
76 utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo},
77 },
78};
79
80pub async fn parse_input(
81 input: &str,
82 external_input_parsers: Option<Vec<ExternalInputParser>>,
83) -> Result<InputType, SdkError> {
84 Ok(breez_sdk_common::input::parse(
85 input,
86 external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
87 )
88 .await?
89 .into())
90}
91
92#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
93const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
94
95#[cfg(all(target_family = "wasm", target_os = "unknown"))]
96const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
97
98const LNURL_METADATA_LIMIT: u32 = 100;
99
100const CLAIM_TX_SIZE_VBYTES: u64 = 99;
101
102bitflags! {
103 #[derive(Clone, Debug)]
104 struct SyncType: u32 {
105 const Wallet = 1 << 0;
106 const WalletState = 1 << 1;
107 const Deposits = 1 << 2;
108 const LnurlMetadata = 1 << 3;
109 const Full = Self::Wallet.0.0
110 | Self::WalletState.0.0
111 | Self::Deposits.0.0
112 | Self::LnurlMetadata.0.0;
113 }
114}
115
116#[derive(Clone, Debug)]
117struct SyncRequest {
118 sync_type: SyncType,
119 #[allow(clippy::type_complexity)]
120 reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
121}
122
123impl SyncRequest {
124 fn new(reply: oneshot::Sender<Result<(), SdkError>>, sync_type: SyncType) -> Self {
125 Self {
126 sync_type,
127 reply: Arc::new(Mutex::new(Some(reply))),
128 }
129 }
130
131 fn full(reply: Option<oneshot::Sender<Result<(), SdkError>>>) -> Self {
132 Self {
133 sync_type: SyncType::Full,
134 reply: Arc::new(Mutex::new(reply)),
135 }
136 }
137
138 fn no_reply(sync_type: SyncType) -> Self {
139 Self {
140 sync_type,
141 reply: Arc::new(Mutex::new(None)),
142 }
143 }
144
145 async fn reply(&self, error: Option<SdkError>) {
146 if let Some(reply) = self.reply.lock().await.take() {
147 let _ = match error {
148 Some(e) => reply.send(Err(e)),
149 None => reply.send(Ok(())),
150 };
151 }
152 }
153}
154
155#[derive(Clone)]
158#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
159pub struct BreezSdk {
160 config: Config,
161 spark_wallet: Arc<SparkWallet>,
162 storage: Arc<dyn Storage>,
163 chain_service: Arc<dyn BitcoinChainService>,
164 fiat_service: Arc<dyn FiatService>,
165 lnurl_client: Arc<dyn RestClient>,
166 lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
167 event_emitter: Arc<EventEmitter>,
168 shutdown_sender: watch::Sender<()>,
169 sync_trigger: tokio::sync::broadcast::Sender<SyncRequest>,
170 zap_receipt_trigger: tokio::sync::broadcast::Sender<()>,
171 initial_synced_watcher: watch::Receiver<bool>,
172 external_input_parsers: Vec<ExternalInputParser>,
173 spark_private_mode_initialized: Arc<OnceCell<()>>,
174 nostr_client: Arc<NostrClient>,
175}
176
177#[cfg_attr(feature = "uniffi", uniffi::export)]
178pub fn init_logging(
179 log_dir: Option<String>,
180 app_logger: Option<Box<dyn Logger>>,
181 log_filter: Option<String>,
182) -> Result<(), SdkError> {
183 logger::init_logging(log_dir, app_logger, log_filter)
184}
185
186#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
196#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
197pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
198 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
199 .with_default_storage(request.storage_dir);
200 let sdk = builder.build().await?;
201 Ok(sdk)
202}
203
204#[cfg_attr(feature = "uniffi", uniffi::export)]
205pub fn default_config(network: Network) -> Config {
206 let lnurl_domain = match network {
207 Network::Mainnet => Some("breez.tips".to_string()),
208 Network::Regtest => None,
209 };
210 Config {
211 api_key: None,
212 network,
213 sync_interval_secs: 60, max_deposit_claim_fee: Some(MaxFee::Rate { sat_per_vbyte: 1 }),
215 lnurl_domain,
216 prefer_spark_over_lightning: false,
217 external_input_parsers: None,
218 use_default_external_input_parsers: true,
219 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
220 private_enabled_default: true,
221 }
222}
223
224pub(crate) struct BreezSdkParams {
225 pub config: Config,
226 pub storage: Arc<dyn Storage>,
227 pub chain_service: Arc<dyn BitcoinChainService>,
228 pub fiat_service: Arc<dyn FiatService>,
229 pub lnurl_client: Arc<dyn RestClient>,
230 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
231 pub shutdown_sender: watch::Sender<()>,
232 pub spark_wallet: Arc<SparkWallet>,
233 pub event_emitter: Arc<EventEmitter>,
234 pub nostr_client: Arc<NostrClient>,
235}
236
237impl BreezSdk {
238 pub(crate) fn init_and_start(params: BreezSdkParams) -> Result<Self, SdkError> {
240 if !matches!(params.config.network, Network::Regtest) {
243 match ¶ms.config.api_key {
244 Some(api_key) => validate_breez_api_key(api_key)?,
245 None => return Err(SdkError::Generic("Missing Breez API key".to_string())),
246 }
247 }
248 let (initial_synced_sender, initial_synced_watcher) = watch::channel(false);
249 let external_input_parsers = params.config.get_all_external_input_parsers();
250 let sdk = Self {
251 config: params.config,
252 spark_wallet: params.spark_wallet,
253 storage: params.storage,
254 chain_service: params.chain_service,
255 fiat_service: params.fiat_service,
256 lnurl_client: params.lnurl_client,
257 lnurl_server_client: params.lnurl_server_client,
258 event_emitter: params.event_emitter,
259 shutdown_sender: params.shutdown_sender,
260 sync_trigger: tokio::sync::broadcast::channel(10).0,
261 zap_receipt_trigger: tokio::sync::broadcast::channel(10).0,
262 initial_synced_watcher,
263 external_input_parsers,
264 spark_private_mode_initialized: Arc::new(OnceCell::new()),
265 nostr_client: params.nostr_client,
266 };
267
268 sdk.start(initial_synced_sender);
269 Ok(sdk)
270 }
271
272 fn start(&self, initial_synced_sender: watch::Sender<bool>) {
280 self.spawn_spark_private_mode_initialization();
281 self.periodic_sync(initial_synced_sender);
282 self.try_recover_lightning_address();
283 self.spawn_zap_receipt_publisher();
284 }
285
286 fn spawn_spark_private_mode_initialization(&self) {
287 let sdk = self.clone();
288 tokio::spawn(async move {
289 if let Err(e) = sdk.ensure_spark_private_mode_initialized().await {
290 error!("Failed to initialize spark private mode: {e:?}");
291 }
292 });
293 }
294
295 fn try_recover_lightning_address(&self) {
297 let sdk = self.clone();
298 tokio::spawn(async move {
299 if sdk.config.lnurl_domain.is_none() {
300 return;
301 }
302
303 match sdk.recover_lightning_address().await {
304 Ok(None) => info!("no lightning address to recover on startup"),
305 Ok(Some(value)) => info!(
306 "recovered lightning address on startup: lnurl: {}, address: {}",
307 value.lnurl, value.lightning_address
308 ),
309 Err(e) => error!("Failed to recover lightning address on startup: {e:?}"),
310 }
311 });
312 }
313
314 fn spawn_zap_receipt_publisher(&self) {
317 let sdk = self.clone();
318 let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
319 let mut trigger_receiver = sdk.zap_receipt_trigger.clone().subscribe();
320
321 tokio::spawn(async move {
322 if let Err(e) = Self::process_pending_zap_receipts(&sdk).await {
323 error!("Failed to process pending zap receipts on startup: {e:?}");
324 }
325
326 loop {
327 tokio::select! {
328 _ = shutdown_receiver.changed() => {
329 info!("Zap receipt publisher shutdown signal received");
330 return;
331 }
332 _ = trigger_receiver.recv() => {
333 if let Err(e) = Self::process_pending_zap_receipts(&sdk).await {
334 error!("Failed to process pending zap receipts: {e:?}");
335 }
336 }
337 }
338 }
339 });
340 }
341
342 async fn process_pending_zap_receipts(&self) -> Result<(), SdkError> {
343 let Some(lnurl_server_client) = self.lnurl_server_client.clone() else {
344 return Ok(());
345 };
346
347 let mut offset = 0;
348 let limit = 100;
349 loop {
350 let payments = self
351 .storage
352 .list_payments(ListPaymentsRequest {
353 offset: Some(offset),
354 limit: Some(limit),
355 status_filter: Some(vec![PaymentStatus::Completed]),
356 type_filter: Some(vec![PaymentType::Receive]),
357 asset_filter: Some(AssetFilter::Bitcoin),
358 ..Default::default()
359 })
360 .await?;
361 if payments.is_empty() {
362 break;
363 }
364
365 let len = u32::try_from(payments.len())?;
366 for payment in payments {
367 let Some(PaymentDetails::Lightning {
368 ref lnurl_receive_metadata,
369 ref payment_hash,
370 ..
371 }) = payment.details
372 else {
373 continue;
374 };
375
376 let Some(lnurl_receive_metadata) = lnurl_receive_metadata else {
377 continue;
378 };
379
380 let Some(zap_request) = &lnurl_receive_metadata.nostr_zap_request else {
381 continue;
382 };
383
384 if lnurl_receive_metadata.nostr_zap_receipt.is_some() {
385 continue;
386 }
387
388 let zap_receipt = match self.nostr_client.create_zap_receipt(zap_request, &payment)
390 {
391 Ok(receipt) => receipt,
392 Err(e) => {
393 error!(
394 "Failed to create zap receipt for payment {}: {e:?}",
395 payment.id
396 );
397 continue;
398 }
399 };
400
401 let zap_receipt = match lnurl_server_client
403 .publish_zap_receipt(&PublishZapReceiptRequest {
404 payment_hash: payment_hash.clone(),
405 zap_receipt: zap_receipt.clone(),
406 })
407 .await
408 {
409 Ok(zap_receipt) => zap_receipt,
410 Err(e) => {
411 error!(
412 "Failed to publish zap receipt for payment {}: {}",
413 payment.id, e
414 );
415 continue;
416 }
417 };
418
419 if let Err(e) = self
420 .storage
421 .set_lnurl_metadata(vec![SetLnurlMetadataItem {
422 sender_comment: lnurl_receive_metadata.sender_comment.clone(),
423 nostr_zap_request: Some(zap_request.clone()),
424 nostr_zap_receipt: Some(zap_receipt),
425 payment_hash: payment_hash.clone(),
426 }])
427 .await
428 {
429 error!(
430 "Failed to store zap receipt for payment {}: {}",
431 payment.id, e
432 );
433 }
434 }
435
436 if len < limit {
437 break;
438 }
439
440 offset = offset.saturating_add(len);
441 }
442
443 Ok(())
444 }
445
446 fn periodic_sync(&self, initial_synced_sender: watch::Sender<bool>) {
447 let sdk = self.clone();
448 let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
449 let mut subscription = sdk.spark_wallet.subscribe_events();
450 let sync_trigger_sender = sdk.sync_trigger.clone();
451 let mut sync_trigger_receiver = sdk.sync_trigger.clone().subscribe();
452 let mut last_sync_time = SystemTime::now();
453 let sync_interval = u64::from(self.config.sync_interval_secs);
454 tokio::spawn(async move {
455 let balance_watcher =
456 BalanceWatcher::new(sdk.spark_wallet.clone(), sdk.storage.clone());
457 let balance_watcher_id = sdk.add_event_listener(Box::new(balance_watcher)).await;
458 loop {
459 tokio::select! {
460 _ = shutdown_receiver.changed() => {
461 if !sdk.remove_event_listener(&balance_watcher_id).await {
462 error!("Failed to remove balance watcher listener");
463 }
464 info!("Deposit tracking loop shutdown signal received");
465 return;
466 }
467 event = subscription.recv() => {
468 match event {
469 Ok(event) => {
470 info!("Received event: {event}");
471 trace!("Received event: {:?}", event);
472 sdk.handle_wallet_event(event).await;
473 }
474 Err(e) => {
475 error!("Failed to receive event: {e:?}");
476 }
477 }
478 }
479 sync_type_res = sync_trigger_receiver.recv() => {
480 let Ok(sync_request) = sync_type_res else {
481 continue;
482 };
483 info!("Sync trigger changed: {:?}", &sync_request);
484 let cloned_sdk = sdk.clone();
485 let initial_synced_sender = initial_synced_sender.clone();
486 if let Some(true) = Box::pin(run_with_shutdown(shutdown_receiver.clone(), "Sync trigger changed", async move {
487 if let Err(e) = cloned_sdk.sync_wallet_internal(sync_request.sync_type.clone()).await {
488 error!("Failed to sync wallet: {e:?}");
489 let () = sync_request.reply(Some(e)).await;
490 return false;
491 }
492
493 if sync_request.sync_type.contains(SyncType::Full) {
494 let () = sync_request.reply(None).await;
495 if let Err(e) = initial_synced_sender.send(true) {
496 error!("Failed to send initial synced signal: {e:?}");
497 }
498 return true;
499 }
500
501 false
502 })).await {
503 last_sync_time = SystemTime::now();
504 }
505 }
506 () = tokio::time::sleep(Duration::from_secs(10)) => {
508 let now = SystemTime::now();
509 if let Ok(elapsed) = now.duration_since(last_sync_time) && elapsed.as_secs() >= sync_interval
510 && let Err(e) = sync_trigger_sender.send(SyncRequest::full(None)) {
511 error!("Failed to trigger periodic sync: {e:?}");
512 }
513 }
514 }
515 }
516 });
517 }
518
519 async fn handle_wallet_event(&self, event: WalletEvent) {
520 match event {
521 WalletEvent::DepositConfirmed(_) => {
522 info!("Deposit confirmed");
523 }
524 WalletEvent::StreamConnected => {
525 info!("Stream connected");
526 }
527 WalletEvent::StreamDisconnected => {
528 info!("Stream disconnected");
529 }
530 WalletEvent::Synced => {
531 info!("Synced");
532 if let Err(e) = self.sync_trigger.send(SyncRequest::full(None)) {
533 error!("Failed to sync wallet: {e:?}");
534 }
535 }
536 WalletEvent::TransferClaimed(transfer) => {
537 info!("Transfer claimed");
538 if let Ok(mut payment) = Payment::try_from(transfer) {
539 if let Err(e) = self.storage.insert_payment(payment.clone()).await {
541 error!("Failed to insert succeeded payment: {e:?}");
542 }
543
544 self.sync_single_lnurl_metadata(&mut payment).await;
547
548 self.event_emitter
549 .emit(&SdkEvent::PaymentSucceeded { payment })
550 .await;
551 }
552 if let Err(e) = self
553 .sync_trigger
554 .send(SyncRequest::no_reply(SyncType::WalletState))
555 {
556 error!("Failed to sync wallet: {e:?}");
557 }
558 }
559 WalletEvent::TransferClaimStarting(transfer) => {
560 info!("Transfer claim starting");
561 if let Ok(mut payment) = Payment::try_from(transfer) {
562 if let Err(e) = self.storage.insert_payment(payment.clone()).await {
564 error!("Failed to insert pending payment: {e:?}");
565 }
566
567 self.sync_single_lnurl_metadata(&mut payment).await;
569
570 self.event_emitter
571 .emit(&SdkEvent::PaymentPending { payment })
572 .await;
573 }
574 if let Err(e) = self
575 .sync_trigger
576 .send(SyncRequest::no_reply(SyncType::WalletState))
577 {
578 error!("Failed to sync wallet: {e:?}");
579 }
580 }
581 }
582 }
583
584 async fn sync_single_lnurl_metadata(&self, payment: &mut Payment) {
585 if payment.payment_type != PaymentType::Receive {
586 return;
587 }
588
589 let Some(PaymentDetails::Lightning {
590 invoice,
591 lnurl_receive_metadata,
592 ..
593 }) = &mut payment.details
594 else {
595 return;
596 };
597
598 if lnurl_receive_metadata.is_some() {
599 return;
601 }
602
603 let Ok(input) = parse_input(invoice, None).await else {
604 error!(
605 "Failed to parse invoice for lnurl metadata sync: {}",
606 invoice
607 );
608 return;
609 };
610
611 let InputType::Bolt11Invoice(details) = input else {
612 error!(
613 "Input is not a Bolt11 invoice for lnurl metadata sync: {}",
614 invoice
615 );
616 return;
617 };
618
619 if details.description_hash.is_none() {
621 return;
622 }
623
624 if let Ok(db_payment) = self.storage.get_payment_by_id(payment.id.clone()).await
626 && let Some(PaymentDetails::Lightning {
627 lnurl_receive_metadata: db_lnurl_receive_metadata,
628 ..
629 }) = db_payment.details
630 {
631 *lnurl_receive_metadata = db_lnurl_receive_metadata;
632 return;
633 }
634
635 let (tx, rx) = oneshot::channel();
637 if let Err(e) = self
638 .sync_trigger
639 .send(SyncRequest::new(tx, SyncType::LnurlMetadata))
640 {
641 error!("Failed to trigger lnurl metadata sync: {e}");
642 return;
643 }
644
645 if let Err(e) = rx.await {
646 error!("Failed to sync lnurl metadata for invoice {}: {e}", invoice);
647 return;
648 }
649
650 let db_payment = match self.storage.get_payment_by_id(payment.id.clone()).await {
651 Ok(p) => p,
652 Err(e) => {
653 debug!("Payment not found in storage for invoice {}: {e}", invoice);
654 return;
655 }
656 };
657
658 let Some(PaymentDetails::Lightning {
659 lnurl_receive_metadata: db_lnurl_receive_metadata,
660 ..
661 }) = db_payment.details
662 else {
663 debug!(
664 "No lnurl receive metadata in storage for invoice {}",
665 invoice
666 );
667 return;
668 };
669 *lnurl_receive_metadata = db_lnurl_receive_metadata;
670 }
671
672 #[allow(clippy::too_many_lines)]
673 async fn sync_wallet_internal(&self, sync_type: SyncType) -> Result<(), SdkError> {
674 let start_time = Instant::now();
675
676 let sync_wallet = async {
677 let wallet_synced = if sync_type.contains(SyncType::Wallet) {
678 debug!("sync_wallet_internal: Starting Wallet sync");
679 let wallet_start = Instant::now();
680 match self.spark_wallet.sync().await {
681 Ok(()) => {
682 debug!(
683 "sync_wallet_internal: Wallet sync completed in {:?}",
684 wallet_start.elapsed()
685 );
686 true
687 }
688 Err(e) => {
689 error!(
690 "sync_wallet_internal: Spark wallet sync failed in {:?}: {e:?}",
691 wallet_start.elapsed()
692 );
693 false
694 }
695 }
696 } else {
697 trace!("sync_wallet_internal: Skipping Wallet sync");
698 false
699 };
700
701 let wallet_state_synced = if sync_type.contains(SyncType::WalletState) {
702 debug!("sync_wallet_internal: Starting WalletState sync");
703 let wallet_state_start = Instant::now();
704 match self.sync_wallet_state_to_storage().await {
705 Ok(()) => {
706 debug!(
707 "sync_wallet_internal: WalletState sync completed in {:?}",
708 wallet_state_start.elapsed()
709 );
710 true
711 }
712 Err(e) => {
713 error!(
714 "sync_wallet_internal: Failed to sync wallet state to storage in {:?}: {e:?}",
715 wallet_state_start.elapsed()
716 );
717 false
718 }
719 }
720 } else {
721 trace!("sync_wallet_internal: Skipping WalletState sync");
722 false
723 };
724
725 (wallet_synced, wallet_state_synced)
726 };
727
728 let sync_lnurl = async {
729 if sync_type.contains(SyncType::LnurlMetadata) {
730 debug!("sync_wallet_internal: Starting LnurlMetadata sync");
731 let lnurl_start = Instant::now();
732 match self.sync_lnurl_metadata().await {
733 Ok(()) => {
734 debug!(
735 "sync_wallet_internal: LnurlMetadata sync completed in {:?}",
736 lnurl_start.elapsed()
737 );
738 true
739 }
740 Err(e) => {
741 error!(
742 "sync_wallet_internal: Failed to sync lnurl metadata in {:?}: {e:?}",
743 lnurl_start.elapsed()
744 );
745 false
746 }
747 }
748 } else {
749 trace!("sync_wallet_internal: Skipping LnurlMetadata sync");
750 false
751 }
752 };
753
754 let sync_deposits = async {
755 if sync_type.contains(SyncType::Deposits) {
756 debug!("sync_wallet_internal: Starting Deposits sync");
757 let deposits_start = Instant::now();
758 match self.check_and_claim_static_deposits().await {
759 Ok(()) => {
760 debug!(
761 "sync_wallet_internal: Deposits sync completed in {:?}",
762 deposits_start.elapsed()
763 );
764 true
765 }
766 Err(e) => {
767 error!(
768 "sync_wallet_internal: Failed to check and claim static deposits in {:?}: {e:?}",
769 deposits_start.elapsed()
770 );
771 false
772 }
773 }
774 } else {
775 trace!("sync_wallet_internal: Skipping Deposits sync");
776 false
777 }
778 };
779
780 let ((wallet, wallet_state), lnurl_metadata, deposits) =
781 tokio::join!(sync_wallet, sync_lnurl, sync_deposits);
782
783 let elapsed = start_time.elapsed();
784 let event = InternalSyncedEvent {
785 wallet,
786 wallet_state,
787 lnurl_metadata,
788 deposits,
789 storage_incoming: None,
790 };
791 info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}: {event:?}");
792 self.event_emitter.emit_synced(&event).await;
793 Ok(())
794 }
795
796 async fn sync_wallet_state_to_storage(&self) -> Result<(), SdkError> {
798 update_balances(self.spark_wallet.clone(), self.storage.clone()).await?;
799
800 let sync_service = SparkSyncService::new(self.spark_wallet.clone(), self.storage.clone());
801 sync_service.sync_payments().await?;
802
803 Ok(())
804 }
805
806 async fn check_and_claim_static_deposits(&self) -> Result<(), SdkError> {
807 self.ensure_spark_private_mode_initialized().await?;
808 let to_claim = DepositChainSyncer::new(
809 self.chain_service.clone(),
810 self.storage.clone(),
811 self.spark_wallet.clone(),
812 )
813 .sync()
814 .await?;
815
816 let mut claimed_deposits: Vec<DepositInfo> = Vec::new();
817 let mut unclaimed_deposits: Vec<DepositInfo> = Vec::new();
818 for detailed_utxo in to_claim {
819 match self
820 .claim_utxo(&detailed_utxo, self.config.max_deposit_claim_fee.clone())
821 .await
822 {
823 Ok(_) => {
824 info!("Claimed utxo {}:{}", detailed_utxo.txid, detailed_utxo.vout);
825 self.storage
826 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
827 .await?;
828 claimed_deposits.push(detailed_utxo.into());
829 }
830 Err(e) => {
831 warn!(
832 "Failed to claim utxo {}:{}: {e}",
833 detailed_utxo.txid, detailed_utxo.vout
834 );
835 self.storage
836 .update_deposit(
837 detailed_utxo.txid.to_string(),
838 detailed_utxo.vout,
839 UpdateDepositPayload::ClaimError {
840 error: e.clone().into(),
841 },
842 )
843 .await?;
844 let mut unclaimed_deposit: DepositInfo = detailed_utxo.clone().into();
845 unclaimed_deposit.claim_error = Some(e.into());
846 unclaimed_deposits.push(unclaimed_deposit);
847 }
848 }
849 }
850
851 info!("background claim completed, unclaimed deposits: {unclaimed_deposits:?}");
852
853 if !unclaimed_deposits.is_empty() {
854 self.event_emitter
855 .emit(&SdkEvent::UnclaimedDeposits { unclaimed_deposits })
856 .await;
857 }
858 if !claimed_deposits.is_empty() {
859 self.event_emitter
860 .emit(&SdkEvent::ClaimedDeposits { claimed_deposits })
861 .await;
862 }
863 Ok(())
864 }
865
866 async fn sync_lnurl_metadata(&self) -> Result<(), SdkError> {
867 let Some(lnurl_server_client) = self.lnurl_server_client.clone() else {
868 return Ok(());
869 };
870
871 let cache = ObjectCacheRepository::new(Arc::clone(&self.storage));
872 let mut updated_after = cache.fetch_lnurl_metadata_updated_after().await?;
873
874 loop {
875 debug!("Syncing lnurl metadata from updated_after {updated_after}");
876 let metadata = lnurl_server_client
877 .list_metadata(&ListMetadataRequest {
878 offset: None,
879 limit: Some(LNURL_METADATA_LIMIT),
880 updated_after: Some(updated_after),
881 })
882 .await?;
883
884 if metadata.metadata.is_empty() {
885 debug!("No more lnurl metadata on offset {updated_after}");
886 break;
887 }
888
889 let len = u32::try_from(metadata.metadata.len())?;
890 let last_updated_at = metadata.metadata.last().map(|m| m.updated_at);
891 self.storage
892 .set_lnurl_metadata(metadata.metadata.into_iter().map(From::from).collect())
893 .await?;
894
895 debug!(
896 "Synchronized {} lnurl metadata at updated_after {updated_after}",
897 len
898 );
899 updated_after = last_updated_at.unwrap_or(updated_after);
900 cache
901 .save_lnurl_metadata_updated_after(updated_after)
902 .await?;
903
904 let _ = self.zap_receipt_trigger.send(());
905 if len < LNURL_METADATA_LIMIT {
906 break;
908 }
909 }
910
911 Ok(())
912 }
913
914 async fn claim_utxo(
915 &self,
916 detailed_utxo: &DetailedUtxo,
917 max_claim_fee: Option<MaxFee>,
918 ) -> Result<WalletTransfer, SdkError> {
919 info!(
920 "Fetching static deposit claim quote for deposit tx {}:{} and amount: {}",
921 detailed_utxo.txid, detailed_utxo.vout, detailed_utxo.value
922 );
923 let quote = self
924 .spark_wallet
925 .fetch_static_deposit_claim_quote(detailed_utxo.tx.clone(), Some(detailed_utxo.vout))
926 .await?;
927
928 let spark_requested_fee_sats = detailed_utxo.value.saturating_sub(quote.credit_amount_sats);
929
930 let spark_requested_fee_rate = spark_requested_fee_sats.div_ceil(CLAIM_TX_SIZE_VBYTES);
931
932 let Some(max_deposit_claim_fee) = max_claim_fee else {
933 return Err(SdkError::MaxDepositClaimFeeExceeded {
934 tx: detailed_utxo.txid.to_string(),
935 vout: detailed_utxo.vout,
936 max_fee: None,
937 required_fee_sats: spark_requested_fee_sats,
938 required_fee_rate_sat_per_vbyte: spark_requested_fee_rate,
939 });
940 };
941 let max_fee = max_deposit_claim_fee
942 .to_fee(self.chain_service.as_ref())
943 .await?;
944 let max_fee_sats = max_fee.to_sats(CLAIM_TX_SIZE_VBYTES);
945 info!(
946 "User max fee: {} spark requested fee: {}",
947 max_fee_sats, spark_requested_fee_sats
948 );
949 if spark_requested_fee_sats > max_fee_sats {
950 return Err(SdkError::MaxDepositClaimFeeExceeded {
951 tx: detailed_utxo.txid.to_string(),
952 vout: detailed_utxo.vout,
953 max_fee: Some(max_fee),
954 required_fee_sats: spark_requested_fee_sats,
955 required_fee_rate_sat_per_vbyte: spark_requested_fee_rate,
956 });
957 }
958
959 info!(
960 "Claiming static deposit for utxo {}:{}",
961 detailed_utxo.txid, detailed_utxo.vout
962 );
963 let transfer = self.spark_wallet.claim_static_deposit(quote).await?;
964 info!(
965 "Claimed static deposit transfer for utxo {}:{}, value {}",
966 detailed_utxo.txid, detailed_utxo.vout, transfer.total_value_sat,
967 );
968 Ok(transfer)
969 }
970
971 async fn ensure_spark_private_mode_initialized(&self) -> Result<(), SdkError> {
972 self.spark_private_mode_initialized
973 .get_or_try_init(|| async {
974 let object_repository = ObjectCacheRepository::new(self.storage.clone());
976 let is_initialized = object_repository
977 .fetch_spark_private_mode_initialized()
978 .await?;
979
980 if !is_initialized {
981 self.initialize_spark_private_mode().await?;
983 }
984 Ok::<_, SdkError>(())
985 })
986 .await?;
987 Ok(())
988 }
989
990 async fn initialize_spark_private_mode(&self) -> Result<(), SdkError> {
991 if !self.config.private_enabled_default {
992 ObjectCacheRepository::new(self.storage.clone())
993 .save_spark_private_mode_initialized()
994 .await?;
995 info!("Spark private mode initialized: no changes needed");
996 return Ok(());
997 }
998
999 self.update_user_settings(UpdateUserSettingsRequest {
1001 spark_private_mode_enabled: Some(true),
1002 })
1003 .await?;
1004 ObjectCacheRepository::new(self.storage.clone())
1005 .save_spark_private_mode_initialized()
1006 .await?;
1007 info!("Spark private mode initialized: enabled");
1008 Ok(())
1009 }
1010}
1011
1012#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
1013#[allow(clippy::needless_pass_by_value)]
1014impl BreezSdk {
1015 pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
1025 self.event_emitter.add_listener(listener).await
1026 }
1027
1028 pub async fn remove_event_listener(&self, id: &str) -> bool {
1038 self.event_emitter.remove_listener(id).await
1039 }
1040
1041 pub async fn disconnect(&self) -> Result<(), SdkError> {
1050 info!("Disconnecting Breez SDK");
1051 self.shutdown_sender
1052 .send(())
1053 .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
1054
1055 self.shutdown_sender.closed().await;
1056 info!("Breez SDK disconnected");
1057 Ok(())
1058 }
1059
1060 pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
1061 parse_input(input, Some(self.external_input_parsers.clone())).await
1062 }
1063
1064 #[allow(unused_variables)]
1066 pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
1067 if request.ensure_synced.unwrap_or_default() {
1068 self.initial_synced_watcher
1069 .clone()
1070 .changed()
1071 .await
1072 .map_err(|_| {
1073 SdkError::Generic("Failed to receive initial synced signal".to_string())
1074 })?;
1075 }
1076 let object_repository = ObjectCacheRepository::new(self.storage.clone());
1077 let account_info = object_repository
1078 .fetch_account_info()
1079 .await?
1080 .unwrap_or_default();
1081 Ok(GetInfoResponse {
1082 balance_sats: account_info.balance_sats,
1083 token_balances: account_info.token_balances,
1084 })
1085 }
1086
1087 pub async fn receive_payment(
1088 &self,
1089 request: ReceivePaymentRequest,
1090 ) -> Result<ReceivePaymentResponse, SdkError> {
1091 self.ensure_spark_private_mode_initialized().await?;
1092 match request.payment_method {
1093 ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse {
1094 fee: 0,
1095 payment_request: self
1096 .spark_wallet
1097 .get_spark_address()?
1098 .to_address_string()
1099 .map_err(|e| {
1100 SdkError::Generic(format!("Failed to convert Spark address to string: {e}"))
1101 })?,
1102 }),
1103 ReceivePaymentMethod::SparkInvoice {
1104 amount,
1105 token_identifier,
1106 expiry_time,
1107 description,
1108 sender_public_key,
1109 } => {
1110 let invoice = self.spark_wallet.create_spark_invoice(
1111 amount,
1112 token_identifier.clone(),
1113 expiry_time
1114 .map(|time| {
1115 SystemTime::UNIX_EPOCH
1116 .checked_add(Duration::from_secs(time))
1117 .ok_or(SdkError::Generic("Invalid expiry time".to_string()))
1118 })
1119 .transpose()?,
1120 description,
1121 sender_public_key.map(|key| PublicKey::from_str(&key).unwrap()),
1122 )?;
1123 Ok(ReceivePaymentResponse {
1124 fee: 0,
1125 payment_request: invoice,
1126 })
1127 }
1128 ReceivePaymentMethod::BitcoinAddress => {
1129 let object_repository = ObjectCacheRepository::new(self.storage.clone());
1132
1133 let static_deposit_address =
1135 object_repository.fetch_static_deposit_address().await?;
1136 if let Some(static_deposit_address) = static_deposit_address {
1137 return Ok(ReceivePaymentResponse {
1138 payment_request: static_deposit_address.address.clone(),
1139 fee: 0,
1140 });
1141 }
1142
1143 let deposit_addresses = self
1145 .spark_wallet
1146 .list_static_deposit_addresses(None)
1147 .await?;
1148
1149 let address = match deposit_addresses.items.last() {
1151 Some(address) => address.to_string(),
1152 None => self
1153 .spark_wallet
1154 .generate_deposit_address(true)
1155 .await?
1156 .to_string(),
1157 };
1158
1159 object_repository
1160 .save_static_deposit_address(&StaticDepositAddress {
1161 address: address.clone(),
1162 })
1163 .await?;
1164
1165 Ok(ReceivePaymentResponse {
1166 payment_request: address,
1167 fee: 0,
1168 })
1169 }
1170 ReceivePaymentMethod::Bolt11Invoice {
1171 description,
1172 amount_sats,
1173 } => Ok(ReceivePaymentResponse {
1174 payment_request: self
1175 .spark_wallet
1176 .create_lightning_invoice(
1177 amount_sats.unwrap_or_default(),
1178 Some(InvoiceDescription::Memo(description.clone())),
1179 None,
1180 self.config.prefer_spark_over_lightning,
1181 )
1182 .await?
1183 .invoice,
1184 fee: 0,
1185 }),
1186 }
1187 }
1188
1189 pub async fn claim_htlc_payment(
1190 &self,
1191 request: ClaimHtlcPaymentRequest,
1192 ) -> Result<ClaimHtlcPaymentResponse, SdkError> {
1193 let preimage = Preimage::from_hex(&request.preimage)
1194 .map_err(|_| SdkError::InvalidInput("Invalid preimage".to_string()))?;
1195 let payment_hash = preimage.compute_hash();
1196
1197 let claimable_htlc_transfers = self
1199 .spark_wallet
1200 .list_claimable_htlc_transfers(None)
1201 .await?;
1202 if !claimable_htlc_transfers
1203 .iter()
1204 .filter_map(|t| t.htlc_preimage_request.as_ref())
1205 .any(|p| p.payment_hash == payment_hash)
1206 {
1207 return Err(SdkError::InvalidInput(
1208 "No claimable HTLC with the given payment hash".to_string(),
1209 ));
1210 }
1211
1212 let transfer = self.spark_wallet.claim_htlc(&preimage).await?;
1213 let payment: Payment = transfer.try_into()?;
1214
1215 self.storage.insert_payment(payment.clone()).await?;
1217
1218 Ok(ClaimHtlcPaymentResponse { payment })
1219 }
1220
1221 pub async fn prepare_lnurl_pay(
1222 &self,
1223 request: PrepareLnurlPayRequest,
1224 ) -> Result<PrepareLnurlPayResponse, SdkError> {
1225 let success_data = match validate_lnurl_pay(
1226 self.lnurl_client.as_ref(),
1227 request.amount_sats.saturating_mul(1_000),
1228 &None,
1229 &request.pay_request.clone().into(),
1230 self.config.network.into(),
1231 request.validate_success_action_url,
1232 )
1233 .await?
1234 {
1235 lnurl::pay::ValidatedCallbackResponse::EndpointError { data } => {
1236 return Err(LnurlError::EndpointError(data.reason).into());
1237 }
1238 lnurl::pay::ValidatedCallbackResponse::EndpointSuccess { data } => data,
1239 };
1240
1241 let prepare_response = self
1242 .prepare_send_payment(PrepareSendPaymentRequest {
1243 payment_request: success_data.pr,
1244 amount: Some(request.amount_sats.into()),
1245 token_identifier: None,
1246 })
1247 .await?;
1248
1249 let SendPaymentMethod::Bolt11Invoice {
1250 invoice_details,
1251 lightning_fee_sats,
1252 ..
1253 } = prepare_response.payment_method
1254 else {
1255 return Err(SdkError::Generic(
1256 "Expected Bolt11Invoice payment method".to_string(),
1257 ));
1258 };
1259
1260 Ok(PrepareLnurlPayResponse {
1261 amount_sats: request.amount_sats,
1262 comment: request.comment,
1263 pay_request: request.pay_request,
1264 invoice_details,
1265 fee_sats: lightning_fee_sats,
1266 success_action: success_data.success_action.map(From::from),
1267 })
1268 }
1269
1270 pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
1271 self.ensure_spark_private_mode_initialized().await?;
1272 let mut payment = Box::pin(self.send_payment_internal(
1273 SendPaymentRequest {
1274 prepare_response: PrepareSendPaymentResponse {
1275 payment_method: SendPaymentMethod::Bolt11Invoice {
1276 invoice_details: request.prepare_response.invoice_details,
1277 spark_transfer_fee_sats: None,
1278 lightning_fee_sats: request.prepare_response.fee_sats,
1279 },
1280 amount: request.prepare_response.amount_sats.into(),
1281 token_identifier: None,
1282 },
1283 options: None,
1284 idempotency_key: request.idempotency_key,
1285 },
1286 true,
1287 ))
1288 .await?
1289 .payment;
1290
1291 let success_action = process_success_action(
1292 &payment,
1293 request
1294 .prepare_response
1295 .success_action
1296 .clone()
1297 .map(Into::into)
1298 .as_ref(),
1299 )?;
1300
1301 let lnurl_info = LnurlPayInfo {
1302 ln_address: request.prepare_response.pay_request.address,
1303 comment: request.prepare_response.comment,
1304 domain: Some(request.prepare_response.pay_request.domain),
1305 metadata: Some(request.prepare_response.pay_request.metadata_str),
1306 processed_success_action: success_action.clone().map(From::from),
1307 raw_success_action: request.prepare_response.success_action,
1308 };
1309 let Some(PaymentDetails::Lightning {
1310 lnurl_pay_info,
1311 description,
1312 ..
1313 }) = &mut payment.details
1314 else {
1315 return Err(SdkError::Generic(
1316 "Expected Lightning payment details".to_string(),
1317 ));
1318 };
1319 *lnurl_pay_info = Some(lnurl_info.clone());
1320
1321 let lnurl_description = lnurl_info.extract_description();
1322 description.clone_from(&lnurl_description);
1323
1324 self.storage
1325 .set_payment_metadata(
1326 payment.id.clone(),
1327 PaymentMetadata {
1328 lnurl_pay_info: Some(lnurl_info),
1329 lnurl_description,
1330 ..Default::default()
1331 },
1332 )
1333 .await?;
1334
1335 emit_payment_status(&self.event_emitter, payment.clone()).await;
1336 Ok(LnurlPayResponse {
1337 payment,
1338 success_action: success_action.map(From::from),
1339 })
1340 }
1341
1342 pub async fn lnurl_withdraw(
1368 &self,
1369 request: LnurlWithdrawRequest,
1370 ) -> Result<LnurlWithdrawResponse, SdkError> {
1371 self.ensure_spark_private_mode_initialized().await?;
1372 let LnurlWithdrawRequest {
1373 amount_sats,
1374 withdraw_request,
1375 completion_timeout_secs,
1376 } = request;
1377 let withdraw_request: breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails =
1378 withdraw_request.into();
1379 if !withdraw_request.is_amount_valid(amount_sats) {
1380 return Err(SdkError::InvalidInput(
1381 "Amount must be within min/max LNURL withdrawable limits".to_string(),
1382 ));
1383 }
1384
1385 let payment_request = self
1387 .receive_payment(ReceivePaymentRequest {
1388 payment_method: ReceivePaymentMethod::Bolt11Invoice {
1389 description: withdraw_request.default_description.clone(),
1390 amount_sats: Some(amount_sats),
1391 },
1392 })
1393 .await?
1394 .payment_request;
1395
1396 let cache = ObjectCacheRepository::new(self.storage.clone());
1398 cache
1399 .save_payment_request_metadata(&PaymentRequestMetadata {
1400 payment_request: payment_request.clone(),
1401 lnurl_withdraw_request_details: withdraw_request.clone(),
1402 })
1403 .await?;
1404
1405 let withdraw_response = execute_lnurl_withdraw(
1407 self.lnurl_client.as_ref(),
1408 &withdraw_request,
1409 &payment_request,
1410 )
1411 .await?;
1412 if let lnurl::withdraw::ValidatedCallbackResponse::EndpointError { data } =
1413 withdraw_response
1414 {
1415 return Err(LnurlError::EndpointError(data.reason).into());
1416 }
1417
1418 let completion_timeout_secs = match completion_timeout_secs {
1419 Some(secs) if secs > 0 => secs,
1420 _ => {
1421 return Ok(LnurlWithdrawResponse {
1422 payment_request,
1423 payment: None,
1424 });
1425 }
1426 };
1427
1428 let payment = self
1430 .wait_for_payment(
1431 WaitForPaymentIdentifier::PaymentRequest(payment_request.clone()),
1432 completion_timeout_secs,
1433 )
1434 .await
1435 .ok();
1436 Ok(LnurlWithdrawResponse {
1437 payment_request,
1438 payment,
1439 })
1440 }
1441
1442 #[allow(clippy::too_many_lines)]
1443 pub async fn prepare_send_payment(
1444 &self,
1445 request: PrepareSendPaymentRequest,
1446 ) -> Result<PrepareSendPaymentResponse, SdkError> {
1447 let parsed_input = self.parse(&request.payment_request).await?;
1448
1449 validate_prepare_send_payment_request(
1450 &parsed_input,
1451 &request,
1452 &self.spark_wallet.get_identity_public_key().to_string(),
1453 )?;
1454
1455 match &parsed_input {
1456 InputType::SparkAddress(spark_address_details) => Ok(PrepareSendPaymentResponse {
1457 payment_method: SendPaymentMethod::SparkAddress {
1458 address: spark_address_details.address.clone(),
1459 fee: 0,
1460 token_identifier: request.token_identifier.clone(),
1461 },
1462 amount: request
1463 .amount
1464 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1465 token_identifier: request.token_identifier,
1466 }),
1467 InputType::SparkInvoice(spark_invoice_details) => Ok(PrepareSendPaymentResponse {
1468 payment_method: SendPaymentMethod::SparkInvoice {
1469 spark_invoice_details: spark_invoice_details.clone(),
1470 fee: 0,
1471 token_identifier: request.token_identifier.clone(),
1472 },
1473 amount: spark_invoice_details
1474 .amount
1475 .or(request.amount)
1476 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1477 token_identifier: request.token_identifier,
1478 }),
1479 InputType::Bolt11Invoice(detailed_bolt11_invoice) => {
1480 let spark_address = self
1481 .spark_wallet
1482 .extract_spark_address(&request.payment_request)?;
1483
1484 let spark_transfer_fee_sats = if spark_address.is_some() {
1485 Some(0)
1486 } else {
1487 None
1488 };
1489
1490 let lightning_fee_sats = self
1491 .spark_wallet
1492 .fetch_lightning_send_fee_estimate(
1493 &request.payment_request,
1494 request
1495 .amount
1496 .map(|a| Ok::<u64, SdkError>(a.try_into()?))
1497 .transpose()?,
1498 )
1499 .await?;
1500
1501 Ok(PrepareSendPaymentResponse {
1502 payment_method: SendPaymentMethod::Bolt11Invoice {
1503 invoice_details: detailed_bolt11_invoice.clone(),
1504 spark_transfer_fee_sats,
1505 lightning_fee_sats,
1506 },
1507 amount: request
1508 .amount
1509 .or(detailed_bolt11_invoice
1510 .amount_msat
1511 .map(|msat| u128::from(msat) / 1000))
1512 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1513 token_identifier: None,
1514 })
1515 }
1516 InputType::BitcoinAddress(withdrawal_address) => {
1517 let fee_quote = self
1518 .spark_wallet
1519 .fetch_coop_exit_fee_quote(
1520 &withdrawal_address.address,
1521 Some(
1522 request
1523 .amount
1524 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?
1525 .try_into()?,
1526 ),
1527 )
1528 .await?;
1529 Ok(PrepareSendPaymentResponse {
1530 payment_method: SendPaymentMethod::BitcoinAddress {
1531 address: withdrawal_address.clone(),
1532 fee_quote: fee_quote.into(),
1533 },
1534 amount: request
1535 .amount
1536 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1537 token_identifier: None,
1538 })
1539 }
1540 _ => Err(SdkError::InvalidInput(
1541 "Unsupported payment method".to_string(),
1542 )),
1543 }
1544 }
1545
1546 pub async fn send_payment(
1547 &self,
1548 request: SendPaymentRequest,
1549 ) -> Result<SendPaymentResponse, SdkError> {
1550 self.ensure_spark_private_mode_initialized().await?;
1551 Box::pin(self.send_payment_internal(request, false)).await
1552 }
1553
1554 #[allow(unused_variables)]
1556 pub async fn sync_wallet(
1557 &self,
1558 request: SyncWalletRequest,
1559 ) -> Result<SyncWalletResponse, SdkError> {
1560 let (tx, rx) = oneshot::channel();
1561
1562 if let Err(e) = self.sync_trigger.send(SyncRequest::full(Some(tx))) {
1563 error!("Failed to send sync trigger: {e:?}");
1564 }
1565 let _ = rx.await.map_err(|e| {
1566 error!("Failed to receive sync trigger: {e:?}");
1567 SdkError::Generic(format!("sync trigger failed: {e:?}"))
1568 })?;
1569 Ok(SyncWalletResponse {})
1570 }
1571
1572 pub async fn list_payments(
1587 &self,
1588 request: ListPaymentsRequest,
1589 ) -> Result<ListPaymentsResponse, SdkError> {
1590 let payments = self.storage.list_payments(request).await?;
1591 Ok(ListPaymentsResponse { payments })
1592 }
1593
1594 pub async fn get_payment(
1595 &self,
1596 request: GetPaymentRequest,
1597 ) -> Result<GetPaymentResponse, SdkError> {
1598 let payment = self.storage.get_payment_by_id(request.payment_id).await?;
1599 Ok(GetPaymentResponse { payment })
1600 }
1601
1602 pub async fn claim_deposit(
1603 &self,
1604 request: ClaimDepositRequest,
1605 ) -> Result<ClaimDepositResponse, SdkError> {
1606 self.ensure_spark_private_mode_initialized().await?;
1607 let detailed_utxo =
1608 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1609 .fetch_detailed_utxo(&request.txid, request.vout)
1610 .await?;
1611
1612 let max_fee = request
1613 .max_fee
1614 .or(self.config.max_deposit_claim_fee.clone());
1615 match self.claim_utxo(&detailed_utxo, max_fee).await {
1616 Ok(transfer) => {
1617 self.storage
1618 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
1619 .await?;
1620 if let Err(e) = self
1621 .sync_trigger
1622 .send(SyncRequest::no_reply(SyncType::WalletState))
1623 {
1624 error!("Failed to execute sync after deposit claim: {e:?}");
1625 }
1626 Ok(ClaimDepositResponse {
1627 payment: transfer.try_into()?,
1628 })
1629 }
1630 Err(e) => {
1631 error!("Failed to claim deposit: {e:?}");
1632 self.storage
1633 .update_deposit(
1634 detailed_utxo.txid.to_string(),
1635 detailed_utxo.vout,
1636 UpdateDepositPayload::ClaimError {
1637 error: e.clone().into(),
1638 },
1639 )
1640 .await?;
1641 Err(e)
1642 }
1643 }
1644 }
1645
1646 pub async fn refund_deposit(
1647 &self,
1648 request: RefundDepositRequest,
1649 ) -> Result<RefundDepositResponse, SdkError> {
1650 let detailed_utxo =
1651 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1652 .fetch_detailed_utxo(&request.txid, request.vout)
1653 .await?;
1654 let tx = self
1655 .spark_wallet
1656 .refund_static_deposit(
1657 detailed_utxo.clone().tx,
1658 Some(detailed_utxo.vout),
1659 &request.destination_address,
1660 request.fee.into(),
1661 )
1662 .await?;
1663 let deposit: DepositInfo = detailed_utxo.into();
1664 let tx_hex = serialize(&tx).as_hex().to_string();
1665 let tx_id = tx.compute_txid().as_raw_hash().to_string();
1666
1667 self.storage
1669 .update_deposit(
1670 deposit.txid.clone(),
1671 deposit.vout,
1672 UpdateDepositPayload::Refund {
1673 refund_tx: tx_hex.clone(),
1674 refund_txid: tx_id.clone(),
1675 },
1676 )
1677 .await?;
1678
1679 self.chain_service
1680 .broadcast_transaction(tx_hex.clone())
1681 .await?;
1682 Ok(RefundDepositResponse { tx_id, tx_hex })
1683 }
1684
1685 #[allow(unused_variables)]
1686 pub async fn list_unclaimed_deposits(
1687 &self,
1688 request: ListUnclaimedDepositsRequest,
1689 ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
1690 let deposits = self.storage.list_deposits().await?;
1691 Ok(ListUnclaimedDepositsResponse { deposits })
1692 }
1693
1694 pub async fn check_lightning_address_available(
1695 &self,
1696 req: CheckLightningAddressRequest,
1697 ) -> Result<bool, SdkError> {
1698 let Some(client) = &self.lnurl_server_client else {
1699 return Err(SdkError::Generic(
1700 "LNURL server is not configured".to_string(),
1701 ));
1702 };
1703
1704 let username = sanitize_username(&req.username);
1705 let available = client.check_username_available(&username).await?;
1706 Ok(available)
1707 }
1708
1709 pub async fn get_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
1710 let cache = ObjectCacheRepository::new(self.storage.clone());
1711 Ok(cache.fetch_lightning_address().await?)
1712 }
1713
1714 pub async fn register_lightning_address(
1715 &self,
1716 request: RegisterLightningAddressRequest,
1717 ) -> Result<LightningAddressInfo, SdkError> {
1718 self.ensure_spark_private_mode_initialized().await?;
1720
1721 self.register_lightning_address_internal(request).await
1722 }
1723
1724 pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
1725 let cache = ObjectCacheRepository::new(self.storage.clone());
1726 let Some(address_info) = cache.fetch_lightning_address().await? else {
1727 return Ok(());
1728 };
1729
1730 let Some(client) = &self.lnurl_server_client else {
1731 return Err(SdkError::Generic(
1732 "LNURL server is not configured".to_string(),
1733 ));
1734 };
1735
1736 let params = crate::lnurl::UnregisterLightningAddressRequest {
1737 username: address_info.username,
1738 };
1739
1740 client.unregister_lightning_address(¶ms).await?;
1741 cache.delete_lightning_address().await?;
1742 Ok(())
1743 }
1744
1745 pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
1748 let currencies = self
1749 .fiat_service
1750 .fetch_fiat_currencies()
1751 .await?
1752 .into_iter()
1753 .map(From::from)
1754 .collect();
1755 Ok(ListFiatCurrenciesResponse { currencies })
1756 }
1757
1758 pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
1760 let rates = self
1761 .fiat_service
1762 .fetch_fiat_rates()
1763 .await?
1764 .into_iter()
1765 .map(From::from)
1766 .collect();
1767 Ok(ListFiatRatesResponse { rates })
1768 }
1769
1770 pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
1772 Ok(self.chain_service.recommended_fees().await?)
1773 }
1774
1775 pub async fn get_tokens_metadata(
1782 &self,
1783 request: GetTokensMetadataRequest,
1784 ) -> Result<GetTokensMetadataResponse, SdkError> {
1785 let metadata = get_tokens_metadata_cached_or_query(
1786 &self.spark_wallet,
1787 &ObjectCacheRepository::new(self.storage.clone()),
1788 &request
1789 .token_identifiers
1790 .iter()
1791 .map(String::as_str)
1792 .collect::<Vec<_>>(),
1793 )
1794 .await?;
1795 Ok(GetTokensMetadataResponse {
1796 tokens_metadata: metadata,
1797 })
1798 }
1799
1800 pub async fn sign_message(
1804 &self,
1805 request: SignMessageRequest,
1806 ) -> Result<SignMessageResponse, SdkError> {
1807 let pubkey = self.spark_wallet.get_identity_public_key().to_string();
1808 let signature = self.spark_wallet.sign_message(&request.message).await?;
1809 let signature_hex = if request.compact {
1810 signature.serialize_compact().to_lower_hex_string()
1811 } else {
1812 signature.serialize_der().to_lower_hex_string()
1813 };
1814
1815 Ok(SignMessageResponse {
1816 pubkey,
1817 signature: signature_hex,
1818 })
1819 }
1820
1821 pub async fn check_message(
1825 &self,
1826 request: CheckMessageRequest,
1827 ) -> Result<CheckMessageResponse, SdkError> {
1828 let pubkey = PublicKey::from_str(&request.pubkey)
1829 .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
1830 let signature_bytes = hex::decode(&request.signature)
1831 .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
1832 let signature = Signature::from_der(&signature_bytes)
1833 .or_else(|_| Signature::from_compact(&signature_bytes))
1834 .map_err(|_| {
1835 SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
1836 })?;
1837
1838 let is_valid = self
1839 .spark_wallet
1840 .verify_message(&request.message, &signature, &pubkey)
1841 .await
1842 .is_ok();
1843 Ok(CheckMessageResponse { is_valid })
1844 }
1845
1846 pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
1850 self.ensure_spark_private_mode_initialized().await?;
1852
1853 let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
1854
1855 Ok(UserSettings {
1858 spark_private_mode_enabled: spark_user_settings.private_enabled,
1859 })
1860 }
1861
1862 pub async fn update_user_settings(
1866 &self,
1867 request: UpdateUserSettingsRequest,
1868 ) -> Result<(), SdkError> {
1869 if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
1870 self.spark_wallet
1871 .update_wallet_settings(spark_private_mode_enabled)
1872 .await?;
1873
1874 let lightning_address = match self.get_lightning_address().await {
1876 Ok(lightning_address) => lightning_address,
1877 Err(e) => {
1878 error!("Failed to get lightning address during user settings update: {e:?}");
1879 return Ok(());
1880 }
1881 };
1882 let Some(lightning_address) = lightning_address else {
1883 return Ok(());
1884 };
1885 if let Err(e) = self
1886 .register_lightning_address_internal(RegisterLightningAddressRequest {
1887 username: lightning_address.username,
1888 description: Some(lightning_address.description),
1889 })
1890 .await
1891 {
1892 error!("Failed to reregister lightning address during user settings update: {e:?}");
1893 }
1894 }
1895 Ok(())
1896 }
1897
1898 pub fn get_token_issuer(&self) -> TokenIssuer {
1900 TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
1901 }
1902}
1903
1904impl BreezSdk {
1906 async fn send_payment_internal(
1907 &self,
1908 request: SendPaymentRequest,
1909 suppress_payment_event: bool,
1910 ) -> Result<SendPaymentResponse, SdkError> {
1911 if request.idempotency_key.is_some() && request.prepare_response.token_identifier.is_some()
1912 {
1913 return Err(SdkError::InvalidInput(
1914 "Idempotency key is not supported for token payments".to_string(),
1915 ));
1916 }
1917 if let Some(idempotency_key) = &request.idempotency_key {
1918 if let Ok(payment) = self
1920 .storage
1921 .get_payment_by_id(idempotency_key.clone())
1922 .await
1923 {
1924 return Ok(SendPaymentResponse { payment });
1925 }
1926 }
1927
1928 let res = match &request.prepare_response.payment_method {
1929 SendPaymentMethod::SparkAddress {
1930 address,
1931 token_identifier,
1932 ..
1933 } => {
1934 self.send_spark_address(
1935 address,
1936 token_identifier.clone(),
1937 request.prepare_response.amount,
1938 request.options.as_ref(),
1939 request.idempotency_key,
1940 )
1941 .await
1942 }
1943 SendPaymentMethod::SparkInvoice {
1944 spark_invoice_details,
1945 ..
1946 } => {
1947 self.send_spark_invoice(&spark_invoice_details.invoice, &request)
1948 .await
1949 }
1950 SendPaymentMethod::Bolt11Invoice {
1951 invoice_details,
1952 spark_transfer_fee_sats,
1953 lightning_fee_sats,
1954 } => {
1955 Box::pin(self.send_bolt11_invoice(
1956 invoice_details,
1957 *spark_transfer_fee_sats,
1958 *lightning_fee_sats,
1959 &request,
1960 ))
1961 .await
1962 }
1963 SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
1964 self.send_bitcoin_address(address, fee_quote, &request)
1965 .await
1966 }
1967 };
1968 if let Ok(response) = &res {
1969 if !suppress_payment_event {
1970 emit_payment_status(&self.event_emitter, response.payment.clone()).await;
1971 }
1972 if let Err(e) = self
1973 .sync_trigger
1974 .send(SyncRequest::no_reply(SyncType::WalletState))
1975 {
1976 error!("Failed to send sync trigger: {e:?}");
1977 }
1978 }
1979 res
1980 }
1981
1982 async fn send_spark_address(
1983 &self,
1984 address: &str,
1985 token_identifier: Option<String>,
1986 amount: u128,
1987 options: Option<&SendPaymentOptions>,
1988 idempotency_key: Option<String>,
1989 ) -> Result<SendPaymentResponse, SdkError> {
1990 let spark_address = address
1991 .parse::<SparkAddress>()
1992 .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
1993
1994 if let Some(SendPaymentOptions::SparkAddress { htlc_options }) = options
1996 && let Some(htlc_options) = htlc_options
1997 {
1998 if token_identifier.is_some() {
1999 return Err(SdkError::InvalidInput(
2000 "Can't provide both token identifier and HTLC options".to_string(),
2001 ));
2002 }
2003
2004 return self
2005 .send_spark_htlc(
2006 &spark_address,
2007 amount.try_into()?,
2008 htlc_options,
2009 idempotency_key,
2010 )
2011 .await;
2012 }
2013
2014 let payment = if let Some(identifier) = token_identifier {
2015 self.send_spark_token_address(identifier, amount, spark_address)
2016 .await?
2017 } else {
2018 let transfer_id = idempotency_key
2019 .as_ref()
2020 .map(|key| TransferId::from_str(key))
2021 .transpose()?;
2022 let transfer = self
2023 .spark_wallet
2024 .transfer(amount.try_into()?, &spark_address, transfer_id)
2025 .await?;
2026 transfer.try_into()?
2027 };
2028
2029 self.storage.insert_payment(payment.clone()).await?;
2031
2032 Ok(SendPaymentResponse { payment })
2033 }
2034
2035 async fn send_spark_htlc(
2036 &self,
2037 address: &SparkAddress,
2038 amount_sat: u64,
2039 htlc_options: &SparkHtlcOptions,
2040 idempotency_key: Option<String>,
2041 ) -> Result<SendPaymentResponse, SdkError> {
2042 let payment_hash = sha256::Hash::from_str(&htlc_options.payment_hash)
2043 .map_err(|_| SdkError::InvalidInput("Invalid payment hash".to_string()))?;
2044
2045 if htlc_options.expiry_duration_secs == 0 {
2046 return Err(SdkError::InvalidInput(
2047 "Expiry duration must be greater than 0".to_string(),
2048 ));
2049 }
2050 let expiry_duration = Duration::from_secs(htlc_options.expiry_duration_secs);
2051
2052 let transfer_id = idempotency_key
2053 .as_ref()
2054 .map(|key| TransferId::from_str(key))
2055 .transpose()?;
2056 let transfer = self
2057 .spark_wallet
2058 .create_htlc(
2059 amount_sat,
2060 address,
2061 &payment_hash,
2062 expiry_duration,
2063 transfer_id,
2064 )
2065 .await?;
2066
2067 let payment: Payment = transfer.try_into()?;
2068
2069 self.storage.insert_payment(payment.clone()).await?;
2071
2072 Ok(SendPaymentResponse { payment })
2073 }
2074
2075 async fn send_spark_token_address(
2076 &self,
2077 token_identifier: String,
2078 amount: u128,
2079 receiver_address: SparkAddress,
2080 ) -> Result<Payment, SdkError> {
2081 let token_transaction = self
2082 .spark_wallet
2083 .transfer_tokens(
2084 vec![TransferTokenOutput {
2085 token_id: token_identifier,
2086 amount,
2087 receiver_address: receiver_address.clone(),
2088 spark_invoice: None,
2089 }],
2090 None,
2091 )
2092 .await?;
2093
2094 map_and_persist_token_transaction(&self.spark_wallet, &self.storage, &token_transaction)
2095 .await
2096 }
2097
2098 async fn send_spark_invoice(
2099 &self,
2100 invoice: &str,
2101 request: &SendPaymentRequest,
2102 ) -> Result<SendPaymentResponse, SdkError> {
2103 let transfer_id = request
2104 .idempotency_key
2105 .as_ref()
2106 .map(|key| TransferId::from_str(key))
2107 .transpose()?;
2108
2109 let payment = match self
2110 .spark_wallet
2111 .fulfill_spark_invoice(invoice, Some(request.prepare_response.amount), transfer_id)
2112 .await?
2113 {
2114 spark_wallet::FulfillSparkInvoiceResult::Transfer(wallet_transfer) => {
2115 (*wallet_transfer).try_into()?
2116 }
2117 spark_wallet::FulfillSparkInvoiceResult::TokenTransaction(token_transaction) => {
2118 map_and_persist_token_transaction(
2119 &self.spark_wallet,
2120 &self.storage,
2121 &token_transaction,
2122 )
2123 .await?
2124 }
2125 };
2126
2127 self.storage.insert_payment(payment.clone()).await?;
2129
2130 Ok(SendPaymentResponse { payment })
2131 }
2132
2133 async fn send_bolt11_invoice(
2134 &self,
2135 invoice_details: &Bolt11InvoiceDetails,
2136 spark_transfer_fee_sats: Option<u64>,
2137 lightning_fee_sats: u64,
2138 request: &SendPaymentRequest,
2139 ) -> Result<SendPaymentResponse, SdkError> {
2140 let amount_to_send = match invoice_details.amount_msat {
2141 Some(_) => None,
2143 None => Some(request.prepare_response.amount),
2145 };
2146 let (prefer_spark, completion_timeout_secs) = match request.options {
2147 Some(SendPaymentOptions::Bolt11Invoice {
2148 prefer_spark,
2149 completion_timeout_secs,
2150 }) => (prefer_spark, completion_timeout_secs),
2151 _ => (self.config.prefer_spark_over_lightning, None),
2152 };
2153 let fee_sats = match (prefer_spark, spark_transfer_fee_sats, lightning_fee_sats) {
2154 (true, Some(fee), _) => fee,
2155 _ => lightning_fee_sats,
2156 };
2157 let transfer_id = request
2158 .idempotency_key
2159 .as_ref()
2160 .map(|idempotency_key| TransferId::from_str(idempotency_key))
2161 .transpose()?;
2162
2163 let payment_response = self
2164 .spark_wallet
2165 .pay_lightning_invoice(
2166 &invoice_details.invoice.bolt11,
2167 amount_to_send
2168 .map(|a| Ok::<u64, SdkError>(a.try_into()?))
2169 .transpose()?,
2170 Some(fee_sats),
2171 prefer_spark,
2172 transfer_id,
2173 )
2174 .await?;
2175 let payment = match payment_response.lightning_payment {
2176 Some(lightning_payment) => {
2177 let ssp_id = lightning_payment.id.clone();
2178 let payment = Payment::from_lightning(
2179 lightning_payment,
2180 request.prepare_response.amount,
2181 payment_response.transfer.id.to_string(),
2182 )?;
2183 self.poll_lightning_send_payment(&payment, ssp_id);
2184 payment
2185 }
2186 None => payment_response.transfer.try_into()?,
2187 };
2188
2189 let Some(completion_timeout_secs) = completion_timeout_secs else {
2190 return Ok(SendPaymentResponse { payment });
2191 };
2192
2193 if completion_timeout_secs == 0 {
2194 return Ok(SendPaymentResponse { payment });
2195 }
2196
2197 let payment = self
2198 .wait_for_payment(
2199 WaitForPaymentIdentifier::PaymentId(payment.id.clone()),
2200 completion_timeout_secs,
2201 )
2202 .await
2203 .unwrap_or(payment);
2204
2205 self.storage.insert_payment(payment.clone()).await?;
2207
2208 Ok(SendPaymentResponse { payment })
2209 }
2210
2211 async fn send_bitcoin_address(
2212 &self,
2213 address: &BitcoinAddressDetails,
2214 fee_quote: &SendOnchainFeeQuote,
2215 request: &SendPaymentRequest,
2216 ) -> Result<SendPaymentResponse, SdkError> {
2217 let exit_speed = match &request.options {
2218 Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) => {
2219 confirmation_speed.clone().into()
2220 }
2221 None => ExitSpeed::Fast,
2222 _ => {
2223 return Err(SdkError::InvalidInput("Invalid options".to_string()));
2224 }
2225 };
2226 let transfer_id = request
2227 .idempotency_key
2228 .as_ref()
2229 .map(|idempotency_key| TransferId::from_str(idempotency_key))
2230 .transpose()?;
2231 let response = self
2232 .spark_wallet
2233 .withdraw(
2234 &address.address,
2235 Some(request.prepare_response.amount.try_into()?),
2236 exit_speed,
2237 fee_quote.clone().into(),
2238 transfer_id,
2239 )
2240 .await?;
2241
2242 let payment: Payment = response.try_into()?;
2243
2244 self.storage.insert_payment(payment.clone()).await?;
2245
2246 Ok(SendPaymentResponse { payment })
2247 }
2248
2249 async fn wait_for_payment(
2250 &self,
2251 identifier: WaitForPaymentIdentifier,
2252 completion_timeout_secs: u32,
2253 ) -> Result<Payment, SdkError> {
2254 let (tx, mut rx) = mpsc::channel(20);
2255 let id = self
2256 .add_event_listener(Box::new(InternalEventListener::new(tx)))
2257 .await;
2258
2259 let payment = match &identifier {
2261 WaitForPaymentIdentifier::PaymentId(payment_id) => self
2262 .storage
2263 .get_payment_by_id(payment_id.clone())
2264 .await
2265 .ok(),
2266 WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
2267 self.storage
2268 .get_payment_by_invoice(payment_request.clone())
2269 .await?
2270 }
2271 };
2272 if let Some(payment) = payment
2273 && payment.status == PaymentStatus::Completed
2274 {
2275 self.remove_event_listener(&id).await;
2276 return Ok(payment);
2277 }
2278
2279 let timeout_res = timeout(Duration::from_secs(completion_timeout_secs.into()), async {
2280 loop {
2281 let Some(event) = rx.recv().await else {
2282 return Err(SdkError::Generic("Event channel closed".to_string()));
2283 };
2284
2285 let SdkEvent::PaymentSucceeded { payment } = event else {
2286 continue;
2287 };
2288
2289 if is_payment_match(&payment, &identifier) {
2290 return Ok(payment);
2291 }
2292 }
2293 })
2294 .await
2295 .map_err(|_| SdkError::Generic("Timeout waiting for payment".to_string()));
2296
2297 self.remove_event_listener(&id).await;
2298 timeout_res?
2299 }
2300
2301 fn poll_lightning_send_payment(&self, payment: &Payment, ssp_id: String) {
2303 const MAX_POLL_ATTEMPTS: u32 = 20;
2304 let payment_id = payment.id.clone();
2305 info!("Polling lightning send payment {}", payment_id);
2306
2307 let spark_wallet = self.spark_wallet.clone();
2308 let sync_trigger = self.sync_trigger.clone();
2309 let event_emitter = self.event_emitter.clone();
2310 let payment = payment.clone();
2311 let payment_id = payment_id.clone();
2312 let mut shutdown = self.shutdown_sender.subscribe();
2313
2314 tokio::spawn(async move {
2315 for i in 0..MAX_POLL_ATTEMPTS {
2316 info!(
2317 "Polling lightning send payment {} attempt {}",
2318 payment_id, i
2319 );
2320 select! {
2321 _ = shutdown.changed() => {
2322 info!("Shutdown signal received");
2323 return;
2324 },
2325 p = spark_wallet.fetch_lightning_send_payment(&ssp_id) => {
2326 if let Ok(Some(p)) = p && let Ok(payment) = Payment::from_lightning(p.clone(), payment.amount, payment.id.clone()) {
2327 info!("Polling payment status = {} {:?}", payment.status, p.status);
2328 if payment.status != PaymentStatus::Pending {
2329 info!("Polling payment completed status = {}", payment.status);
2330 emit_payment_status(&event_emitter, payment.clone()).await;
2331 if let Err(e) = sync_trigger.send(SyncRequest::no_reply(SyncType::WalletState)) {
2332 error!("Failed to send sync trigger: {e:?}");
2333 }
2334 return;
2335 }
2336 }
2337
2338 let sleep_time = if i < 5 {
2339 Duration::from_secs(1)
2340 } else {
2341 Duration::from_secs(i.into())
2342 };
2343 tokio::time::sleep(sleep_time).await;
2344 }
2345 }
2346 }
2347 });
2348 }
2349
2350 async fn recover_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
2352 let cache = ObjectCacheRepository::new(self.storage.clone());
2353
2354 let Some(client) = &self.lnurl_server_client else {
2355 return Err(SdkError::Generic(
2356 "LNURL server is not configured".to_string(),
2357 ));
2358 };
2359 let resp = client.recover_lightning_address().await?;
2360
2361 let result = if let Some(resp) = resp {
2362 let address_info = resp.into();
2363 cache.save_lightning_address(&address_info).await?;
2364 Some(address_info)
2365 } else {
2366 cache.delete_lightning_address().await?;
2367 None
2368 };
2369
2370 Ok(result)
2371 }
2372
2373 async fn register_lightning_address_internal(
2374 &self,
2375 request: RegisterLightningAddressRequest,
2376 ) -> Result<LightningAddressInfo, SdkError> {
2377 let cache = ObjectCacheRepository::new(self.storage.clone());
2378 let Some(client) = &self.lnurl_server_client else {
2379 return Err(SdkError::Generic(
2380 "LNURL server is not configured".to_string(),
2381 ));
2382 };
2383
2384 let username = sanitize_username(&request.username);
2385
2386 let description = match request.description {
2387 Some(description) => description,
2388 None => format!("Pay to {}@{}", username, client.domain()),
2389 };
2390
2391 let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
2393 let nostr_pubkey = if spark_user_settings.private_enabled {
2394 Some(self.nostr_client.nostr_pubkey())
2395 } else {
2396 None
2397 };
2398
2399 let params = crate::lnurl::RegisterLightningAddressRequest {
2400 username: username.clone(),
2401 description: description.clone(),
2402 nostr_pubkey,
2403 };
2404
2405 let response = client.register_lightning_address(¶ms).await?;
2406 let address_info = LightningAddressInfo {
2407 lightning_address: response.lightning_address,
2408 description,
2409 lnurl: response.lnurl,
2410 username,
2411 };
2412 cache.save_lightning_address(&address_info).await?;
2413 Ok(address_info)
2414 }
2415}
2416
2417fn is_payment_match(payment: &Payment, identifier: &WaitForPaymentIdentifier) -> bool {
2418 match identifier {
2419 WaitForPaymentIdentifier::PaymentId(payment_id) => payment.id == *payment_id,
2420 WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
2421 if let Some(details) = &payment.details {
2422 match details {
2423 PaymentDetails::Lightning { invoice, .. } => {
2424 invoice.to_lowercase() == payment_request.to_lowercase()
2425 }
2426 PaymentDetails::Spark {
2427 invoice_details: invoice,
2428 ..
2429 }
2430 | PaymentDetails::Token {
2431 invoice_details: invoice,
2432 ..
2433 } => {
2434 if let Some(invoice) = invoice {
2435 invoice.invoice.to_lowercase() == payment_request.to_lowercase()
2436 } else {
2437 false
2438 }
2439 }
2440 PaymentDetails::Withdraw { tx_id: _ }
2441 | PaymentDetails::Deposit { tx_id: _ } => false,
2442 }
2443 } else {
2444 false
2445 }
2446 }
2447 }
2448}
2449
2450struct BalanceWatcher {
2451 spark_wallet: Arc<SparkWallet>,
2452 storage: Arc<dyn Storage>,
2453}
2454
2455impl BalanceWatcher {
2456 fn new(spark_wallet: Arc<SparkWallet>, storage: Arc<dyn Storage>) -> Self {
2457 Self {
2458 spark_wallet,
2459 storage,
2460 }
2461 }
2462}
2463
2464#[macros::async_trait]
2465impl EventListener for BalanceWatcher {
2466 async fn on_event(&self, event: SdkEvent) {
2467 match event {
2468 SdkEvent::PaymentSucceeded { .. } | SdkEvent::ClaimedDeposits { .. } => {
2469 match update_balances(self.spark_wallet.clone(), self.storage.clone()).await {
2470 Ok(()) => info!("Balance updated successfully"),
2471 Err(e) => error!("Failed to update balance: {e:?}"),
2472 }
2473 }
2474 _ => {}
2475 }
2476 }
2477}
2478
2479async fn update_balances(
2480 spark_wallet: Arc<SparkWallet>,
2481 storage: Arc<dyn Storage>,
2482) -> Result<(), SdkError> {
2483 let balance_sats = spark_wallet.get_balance().await?;
2484 let token_balances = spark_wallet
2485 .get_token_balances()
2486 .await?
2487 .into_iter()
2488 .map(|(k, v)| (k, v.into()))
2489 .collect();
2490 let object_repository = ObjectCacheRepository::new(storage.clone());
2491
2492 object_repository
2493 .save_account_info(&CachedAccountInfo {
2494 balance_sats,
2495 token_balances,
2496 })
2497 .await?;
2498 let identity_public_key = spark_wallet.get_identity_public_key();
2499 info!(
2500 "Balance updated successfully {} for identity {}",
2501 balance_sats, identity_public_key
2502 );
2503 Ok(())
2504}
2505
2506struct InternalEventListener {
2507 tx: mpsc::Sender<SdkEvent>,
2508}
2509
2510impl InternalEventListener {
2511 #[allow(unused)]
2512 pub fn new(tx: mpsc::Sender<SdkEvent>) -> Self {
2513 Self { tx }
2514 }
2515}
2516
2517#[macros::async_trait]
2518impl EventListener for InternalEventListener {
2519 async fn on_event(&self, event: SdkEvent) {
2520 let _ = self.tx.send(event).await;
2521 }
2522}
2523
2524fn process_success_action(
2525 payment: &Payment,
2526 success_action: Option<&SuccessAction>,
2527) -> Result<Option<SuccessActionProcessed>, LnurlError> {
2528 let Some(success_action) = success_action else {
2529 return Ok(None);
2530 };
2531
2532 let data = match success_action {
2533 SuccessAction::Aes { data } => data,
2534 SuccessAction::Message { data } => {
2535 return Ok(Some(SuccessActionProcessed::Message { data: data.clone() }));
2536 }
2537 SuccessAction::Url { data } => {
2538 return Ok(Some(SuccessActionProcessed::Url { data: data.clone() }));
2539 }
2540 };
2541
2542 let Some(PaymentDetails::Lightning { preimage, .. }) = &payment.details else {
2543 return Err(LnurlError::general(format!(
2544 "Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.",
2545 payment.details
2546 )));
2547 };
2548
2549 let Some(preimage) = preimage else {
2550 return Ok(None);
2551 };
2552
2553 let preimage =
2554 sha256::Hash::from_str(preimage).map_err(|_| LnurlError::general("Invalid preimage"))?;
2555 let preimage = preimage.as_byte_array();
2556 let result: AesSuccessActionDataResult = match (data, preimage).try_into() {
2557 Ok(data) => AesSuccessActionDataResult::Decrypted { data },
2558 Err(e) => AesSuccessActionDataResult::ErrorStatus {
2559 reason: e.to_string(),
2560 },
2561 };
2562
2563 Ok(Some(SuccessActionProcessed::Aes { result }))
2564}
2565
2566async fn emit_payment_status(event_emitter: &EventEmitter, payment: Payment) {
2567 match payment.status {
2568 PaymentStatus::Completed => {
2569 event_emitter
2570 .emit(&SdkEvent::PaymentSucceeded { payment })
2571 .await;
2572 }
2573 PaymentStatus::Failed => {
2574 event_emitter
2575 .emit(&SdkEvent::PaymentFailed { payment })
2576 .await;
2577 }
2578 PaymentStatus::Pending => {
2579 event_emitter
2580 .emit(&SdkEvent::PaymentPending { payment })
2581 .await;
2582 }
2583 }
2584}
2585
2586fn validate_breez_api_key(api_key: &str) -> Result<(), SdkError> {
2587 let api_key_decoded = base64::engine::general_purpose::STANDARD
2588 .decode(api_key.as_bytes())
2589 .map_err(|err| {
2590 SdkError::Generic(format!(
2591 "Could not base64 decode the Breez API key: {err:?}"
2592 ))
2593 })?;
2594 let (_rem, cert) = parse_x509_certificate(&api_key_decoded).map_err(|err| {
2595 SdkError::Generic(format!("Invalid certificate for Breez API key: {err:?}"))
2596 })?;
2597
2598 let issuer = cert
2599 .issuer()
2600 .iter_common_name()
2601 .next()
2602 .and_then(|cn| cn.as_str().ok());
2603 match issuer {
2604 Some(common_name) => {
2605 if !common_name.starts_with("Breez") {
2606 return Err(SdkError::Generic(
2607 "Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted"
2608 .to_string()
2609 ));
2610 }
2611 }
2612 _ => {
2613 return Err(SdkError::Generic(
2614 "Could not parse Breez API key certificate: issuer is invalid or not found."
2615 .to_string(),
2616 ));
2617 }
2618 }
2619
2620 Ok(())
2621}