breez_sdk_spark/
sdk.rs

1use base64::Engine;
2use bitcoin::{
3    consensus::serialize,
4    hashes::{Hash, sha256},
5    hex::DisplayHex,
6    secp256k1::{PublicKey, ecdsa::Signature},
7};
8use breez_sdk_common::{
9    fiat::FiatService,
10    lnurl::{self, withdraw::execute_lnurl_withdraw},
11};
12use breez_sdk_common::{
13    lnurl::{
14        error::LnurlError,
15        pay::{
16            AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed, validate_lnurl_pay,
17        },
18    },
19    rest::RestClient,
20};
21use lnurl_models::sanitize_username;
22use spark_wallet::{
23    ExitSpeed, InvoiceDescription, SparkAddress, SparkWallet, TransferTokenOutput, WalletEvent,
24    WalletTransfer,
25};
26use std::{str::FromStr, sync::Arc};
27use tracing::{error, info, trace, warn};
28use web_time::{Duration, SystemTime};
29
30use tokio::{
31    select,
32    sync::{Mutex, OnceCell, mpsc, oneshot, watch},
33    time::timeout,
34};
35use tokio_with_wasm::alias as tokio;
36use web_time::Instant;
37use x509_parser::parse_x509_certificate;
38
39use crate::{
40    BitcoinAddressDetails, BitcoinChainService, Bolt11InvoiceDetails, CheckLightningAddressRequest,
41    CheckMessageRequest, CheckMessageResponse, ClaimDepositRequest, ClaimDepositResponse,
42    DepositInfo, ExternalInputParser, Fee, GetPaymentRequest, GetPaymentResponse,
43    GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, LightningAddressInfo,
44    ListFiatCurrenciesResponse, ListFiatRatesResponse, ListUnclaimedDepositsRequest,
45    ListUnclaimedDepositsResponse, LnurlPayInfo, LnurlPayRequest, LnurlPayResponse,
46    LnurlWithdrawRequest, LnurlWithdrawResponse, Logger, Network, PaymentDetails, PaymentStatus,
47    PrepareLnurlPayRequest, PrepareLnurlPayResponse, RefundDepositRequest, RefundDepositResponse,
48    RegisterLightningAddressRequest, SendOnchainFeeQuote, SendPaymentOptions, SignMessageRequest,
49    SignMessageResponse, UpdateUserSettingsRequest, UserSettings, WaitForPaymentIdentifier,
50    WaitForPaymentRequest, WaitForPaymentResponse,
51    error::SdkError,
52    events::{EventEmitter, EventListener, SdkEvent},
53    issuer::TokenIssuer,
54    lnurl::LnurlServerClient,
55    logger,
56    models::{
57        Config, GetInfoRequest, GetInfoResponse, ListPaymentsRequest, ListPaymentsResponse,
58        Payment, PrepareSendPaymentRequest, PrepareSendPaymentResponse, ReceivePaymentMethod,
59        ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentMethod, SendPaymentRequest,
60        SendPaymentResponse, SyncWalletRequest, SyncWalletResponse,
61    },
62    persist::{
63        CachedAccountInfo, ObjectCacheRepository, PaymentMetadata, PaymentRequestMetadata,
64        StaticDepositAddress, Storage, UpdateDepositPayload,
65    },
66    sync::SparkSyncService,
67    utils::{
68        deposit_chain_syncer::DepositChainSyncer,
69        run_with_shutdown,
70        send_payment_validation::validate_prepare_send_payment_request,
71        token::{get_tokens_metadata_cached_or_query, map_and_persist_token_transaction},
72        utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo},
73    },
74};
75
76pub async fn parse_input(
77    input: &str,
78    external_input_parsers: Option<Vec<ExternalInputParser>>,
79) -> Result<InputType, SdkError> {
80    Ok(breez_sdk_common::input::parse(
81        input,
82        external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
83    )
84    .await?
85    .into())
86}
87
88#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
89const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
90
91#[cfg(all(target_family = "wasm", target_os = "unknown"))]
92const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
93
94#[derive(Clone, Debug)]
95enum SyncType {
96    Full,
97    PaymentsOnly,
98}
99
100#[derive(Clone, Debug)]
101struct SyncRequest {
102    sync_type: SyncType,
103    #[allow(clippy::type_complexity)]
104    reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
105}
106
107impl SyncRequest {
108    fn full(reply: Option<oneshot::Sender<Result<(), SdkError>>>) -> Self {
109        Self {
110            sync_type: SyncType::Full,
111            reply: Arc::new(Mutex::new(reply)),
112        }
113    }
114
115    fn payments_only(reply: Option<oneshot::Sender<Result<(), SdkError>>>) -> Self {
116        Self {
117            sync_type: SyncType::PaymentsOnly,
118            reply: Arc::new(Mutex::new(reply)),
119        }
120    }
121
122    async fn reply(&self, error: Option<SdkError>) {
123        if let Some(reply) = self.reply.lock().await.take() {
124            let _ = match error {
125                Some(e) => reply.send(Err(e)),
126                None => reply.send(Ok(())),
127            };
128        }
129    }
130}
131
132/// `BreezSDK` is a wrapper around `SparkSDK` that provides a more structured API
133/// with request/response objects and comprehensive error handling.
134#[derive(Clone)]
135#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
136pub struct BreezSdk {
137    config: Config,
138    spark_wallet: Arc<SparkWallet>,
139    storage: Arc<dyn Storage>,
140    chain_service: Arc<dyn BitcoinChainService>,
141    fiat_service: Arc<dyn FiatService>,
142    lnurl_client: Arc<dyn RestClient>,
143    lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
144    event_emitter: Arc<EventEmitter>,
145    shutdown_sender: watch::Sender<()>,
146    sync_trigger: tokio::sync::broadcast::Sender<SyncRequest>,
147    initial_synced_watcher: watch::Receiver<bool>,
148    external_input_parsers: Vec<ExternalInputParser>,
149    spark_private_mode_initialized: Arc<OnceCell<()>>,
150}
151
152#[cfg_attr(feature = "uniffi", uniffi::export)]
153pub fn init_logging(
154    log_dir: Option<String>,
155    app_logger: Option<Box<dyn Logger>>,
156    log_filter: Option<String>,
157) -> Result<(), SdkError> {
158    logger::init_logging(log_dir, app_logger, log_filter)
159}
160
161/// Connects to the Spark network using the provided configuration and mnemonic.
162///
163/// # Arguments
164///
165/// * `request` - The connection request object
166///
167/// # Returns
168///
169/// Result containing either the initialized `BreezSdk` or an `SdkError`
170#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
171#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
172pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
173    let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
174        .with_default_storage(request.storage_dir);
175    let sdk = builder.build().await?;
176    Ok(sdk)
177}
178
179#[cfg_attr(feature = "uniffi", uniffi::export)]
180pub fn default_config(network: Network) -> Config {
181    Config {
182        api_key: None,
183        network,
184        sync_interval_secs: 60, // every 1 minute
185        max_deposit_claim_fee: Some(Fee::Rate { sat_per_vbyte: 1 }),
186        lnurl_domain: Some("breez.tips".to_string()),
187        prefer_spark_over_lightning: false,
188        external_input_parsers: None,
189        use_default_external_input_parsers: true,
190        real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
191        private_enabled_default: true,
192    }
193}
194
195pub(crate) struct BreezSdkParams {
196    pub config: Config,
197    pub storage: Arc<dyn Storage>,
198    pub chain_service: Arc<dyn BitcoinChainService>,
199    pub fiat_service: Arc<dyn FiatService>,
200    pub lnurl_client: Arc<dyn RestClient>,
201    pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
202    pub shutdown_sender: watch::Sender<()>,
203    pub spark_wallet: Arc<SparkWallet>,
204    pub event_emitter: Arc<EventEmitter>,
205}
206
207impl BreezSdk {
208    /// Creates a new instance of the `BreezSdk`
209    pub(crate) fn init_and_start(params: BreezSdkParams) -> Result<Self, SdkError> {
210        // In Regtest we allow running without a Breez API key to facilitate local
211        // integration tests. For non-regtest networks, a valid API key is required.
212        if !matches!(params.config.network, Network::Regtest) {
213            match &params.config.api_key {
214                Some(api_key) => validate_breez_api_key(api_key)?,
215                None => return Err(SdkError::Generic("Missing Breez API key".to_string())),
216            }
217        }
218        let (initial_synced_sender, initial_synced_watcher) = watch::channel(false);
219        let external_input_parsers = params.config.get_all_external_input_parsers();
220        let sdk = Self {
221            config: params.config,
222            spark_wallet: params.spark_wallet,
223            storage: params.storage,
224            chain_service: params.chain_service,
225            fiat_service: params.fiat_service,
226            lnurl_client: params.lnurl_client,
227            lnurl_server_client: params.lnurl_server_client,
228            event_emitter: params.event_emitter,
229            shutdown_sender: params.shutdown_sender,
230            sync_trigger: tokio::sync::broadcast::channel(10).0,
231            initial_synced_watcher,
232            external_input_parsers,
233            spark_private_mode_initialized: Arc::new(OnceCell::new()),
234        };
235
236        sdk.start(initial_synced_sender);
237        Ok(sdk)
238    }
239
240    /// Starts the SDK's background tasks
241    ///
242    /// This method initiates the following backround tasks:
243    /// 1. `spawn_spark_private_mode_initialization`: initializes the spark private mode on startup
244    /// 2. `periodic_sync`: syncs the wallet with the Spark network    
245    /// 3. `try_recover_lightning_address`: recovers the lightning address on startup
246    fn start(&self, initial_synced_sender: watch::Sender<bool>) {
247        self.spawn_spark_private_mode_initialization();
248        self.periodic_sync(initial_synced_sender);
249        self.try_recover_lightning_address();
250    }
251
252    fn spawn_spark_private_mode_initialization(&self) {
253        let sdk = self.clone();
254        tokio::spawn(async move {
255            if let Err(e) = sdk.ensure_spark_private_mode_initialized().await {
256                error!("Failed to initialize spark private mode: {e:?}");
257            }
258        });
259    }
260
261    /// Refreshes the user's lightning address on the server on startup.
262    fn try_recover_lightning_address(&self) {
263        let sdk = self.clone();
264        tokio::spawn(async move {
265            if sdk.config.lnurl_domain.is_none() {
266                return;
267            }
268
269            match sdk.recover_lightning_address().await {
270                Ok(None) => info!("no lightning address to recover on startup"),
271                Ok(Some(value)) => info!(
272                    "recovered lightning address on startup: lnurl: {}, address: {}",
273                    value.lnurl, value.lightning_address
274                ),
275                Err(e) => error!("Failed to recover lightning address on startup: {e:?}"),
276            }
277        });
278    }
279
280    fn periodic_sync(&self, initial_synced_sender: watch::Sender<bool>) {
281        let sdk = self.clone();
282        let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
283        let mut subscription = sdk.spark_wallet.subscribe_events();
284        let sync_trigger_sender = sdk.sync_trigger.clone();
285        let mut sync_trigger_receiver = sdk.sync_trigger.clone().subscribe();
286        let mut last_sync_time = SystemTime::now();
287        let sync_interval = u64::from(self.config.sync_interval_secs);
288        tokio::spawn(async move {
289            let balance_watcher =
290                BalanceWatcher::new(sdk.spark_wallet.clone(), sdk.storage.clone());
291            let balance_watcher_id = sdk.add_event_listener(Box::new(balance_watcher)).await;
292            loop {
293                tokio::select! {
294                    _ = shutdown_receiver.changed() => {
295                        if !sdk.remove_event_listener(&balance_watcher_id).await {
296                            error!("Failed to remove balance watcher listener");
297                        }
298                        info!("Deposit tracking loop shutdown signal received");
299                        return;
300                    }
301                    event = subscription.recv() => {
302                        match event {
303                            Ok(event) => {
304                                info!("Received event: {event}");
305                                trace!("Received event: {:?}", event);
306                                sdk.handle_wallet_event(event).await;
307                            }
308                            Err(e) => {
309                                error!("Failed to receive event: {e:?}");
310                            }
311                        }
312                    }
313                    sync_type_res = sync_trigger_receiver.recv() => {
314                      if let Ok(sync_request) = sync_type_res   {
315                          info!("Sync trigger changed: {:?}", &sync_request);
316                          let cloned_sdk = sdk.clone();
317                          let initial_synced_sender = initial_synced_sender.clone();
318                          if let Some(true) = run_with_shutdown(shutdown_receiver.clone(), "Sync trigger changed", async move {
319                          if let Err(e) = cloned_sdk.sync_wallet_internal(sync_request.sync_type.clone()).await {
320                              error!("Failed to sync wallet: {e:?}");
321                              let () = sync_request.reply(Some(e)).await;
322                              return false
323                          }
324                          if matches!(sync_request.sync_type, SyncType::Full) {
325                            let () = sync_request.reply(None).await;
326                            if let Err(e) = initial_synced_sender.send(true) {
327                              error!("Failed to send initial synced signal: {e:?}");
328                            }
329                            return true
330                          }
331                          false
332                        }).await {
333                          last_sync_time = SystemTime::now();
334                        }
335                      }
336                    }
337                    // Ensure we sync at least the configured interval
338                    () = tokio::time::sleep(Duration::from_secs(10)) => {
339                        let now = SystemTime::now();
340                        if let Ok(elapsed) = now.duration_since(last_sync_time) && elapsed.as_secs() >= sync_interval
341                            && let Err(e) = sync_trigger_sender.send(SyncRequest::full(None)) {
342                            error!("Failed to trigger periodic sync: {e:?}");
343                        }
344                    }
345                }
346            }
347        });
348    }
349
350    async fn handle_wallet_event(&self, event: WalletEvent) {
351        match event {
352            WalletEvent::DepositConfirmed(_) => {
353                info!("Deposit confirmed");
354            }
355            WalletEvent::StreamConnected => {
356                info!("Stream connected");
357            }
358            WalletEvent::StreamDisconnected => {
359                info!("Stream disconnected");
360            }
361            WalletEvent::Synced => {
362                info!("Synced");
363                if let Err(e) = self.sync_trigger.send(SyncRequest::full(None)) {
364                    error!("Failed to sync wallet: {e:?}");
365                }
366            }
367            WalletEvent::TransferClaimed(transfer) => {
368                info!("Transfer claimed");
369                if let Ok(payment) = Payment::try_from(transfer) {
370                    // Insert the payment into storage to make it immediately available for listing
371                    if let Err(e) = self.storage.insert_payment(payment.clone()).await {
372                        error!("Failed to insert succeeded payment: {e:?}");
373                    }
374                    self.event_emitter
375                        .emit(&SdkEvent::PaymentSucceeded { payment })
376                        .await;
377                }
378                if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
379                    error!("Failed to sync wallet: {e:?}");
380                }
381            }
382            WalletEvent::TransferClaimStarting(transfer) => {
383                info!("Transfer claim starting");
384                if let Ok(payment) = Payment::try_from(transfer) {
385                    // Insert the payment into storage to make it immediately available for listing
386                    if let Err(e) = self.storage.insert_payment(payment.clone()).await {
387                        error!("Failed to insert pending payment: {e:?}");
388                    }
389                    self.event_emitter
390                        .emit(&SdkEvent::PaymentPending { payment })
391                        .await;
392                }
393                if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
394                    error!("Failed to sync wallet: {e:?}");
395                }
396            }
397        }
398    }
399
400    async fn sync_wallet_internal(&self, sync_type: SyncType) -> Result<(), SdkError> {
401        let start_time = Instant::now();
402        if let SyncType::Full = sync_type {
403            // Sync with the Spark network
404            if let Err(e) = self.spark_wallet.sync().await {
405                error!("sync_wallet_internal: Failed to sync with Spark network: {e:?}");
406            }
407        }
408        if let Err(e) = self.sync_wallet_state_to_storage().await {
409            error!("sync_wallet_internal: Failed to sync wallet state to storage: {e:?}");
410        }
411        if let Err(e) = self.check_and_claim_static_deposits().await {
412            error!("sync_wallet_internal: Failed to check and claim static deposits: {e:?}");
413        }
414        let elapsed = start_time.elapsed();
415        info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}");
416        self.event_emitter.emit(&SdkEvent::Synced {}).await;
417        Ok(())
418    }
419
420    /// Synchronizes wallet state to persistent storage, making sure we have the latest balances and payments.
421    async fn sync_wallet_state_to_storage(&self) -> Result<(), SdkError> {
422        update_balances(self.spark_wallet.clone(), self.storage.clone()).await?;
423
424        let sync_service = SparkSyncService::new(self.spark_wallet.clone(), self.storage.clone());
425        sync_service.sync_payments().await?;
426
427        Ok(())
428    }
429
430    async fn check_and_claim_static_deposits(&self) -> Result<(), SdkError> {
431        self.ensure_spark_private_mode_initialized().await?;
432        let to_claim = DepositChainSyncer::new(
433            self.chain_service.clone(),
434            self.storage.clone(),
435            self.spark_wallet.clone(),
436        )
437        .sync()
438        .await?;
439
440        let mut claimed_deposits: Vec<DepositInfo> = Vec::new();
441        let mut unclaimed_deposits: Vec<DepositInfo> = Vec::new();
442        for detailed_utxo in to_claim {
443            match self
444                .claim_utxo(&detailed_utxo, self.config.max_deposit_claim_fee.clone())
445                .await
446            {
447                Ok(_) => {
448                    info!("Claimed utxo {}:{}", detailed_utxo.txid, detailed_utxo.vout);
449                    self.storage
450                        .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
451                        .await?;
452                    claimed_deposits.push(detailed_utxo.into());
453                }
454                Err(e) => {
455                    warn!(
456                        "Failed to claim utxo {}:{}: {e}",
457                        detailed_utxo.txid, detailed_utxo.vout
458                    );
459                    self.storage
460                        .update_deposit(
461                            detailed_utxo.txid.to_string(),
462                            detailed_utxo.vout,
463                            UpdateDepositPayload::ClaimError {
464                                error: e.clone().into(),
465                            },
466                        )
467                        .await?;
468                    let mut unclaimed_deposit: DepositInfo = detailed_utxo.clone().into();
469                    unclaimed_deposit.claim_error = Some(e.into());
470                    unclaimed_deposits.push(unclaimed_deposit);
471                }
472            }
473        }
474
475        info!("background claim completed, unclaimed deposits: {unclaimed_deposits:?}");
476
477        if !unclaimed_deposits.is_empty() {
478            self.event_emitter
479                .emit(&SdkEvent::UnclaimedDeposits { unclaimed_deposits })
480                .await;
481        }
482        if !claimed_deposits.is_empty() {
483            self.event_emitter
484                .emit(&SdkEvent::ClaimedDeposits { claimed_deposits })
485                .await;
486        }
487        Ok(())
488    }
489
490    async fn claim_utxo(
491        &self,
492        detailed_utxo: &DetailedUtxo,
493        max_claim_fee: Option<Fee>,
494    ) -> Result<WalletTransfer, SdkError> {
495        info!(
496            "Fetching static deposit claim quote for deposit tx {}:{} and amount: {}",
497            detailed_utxo.txid, detailed_utxo.vout, detailed_utxo.value
498        );
499        let quote = self
500            .spark_wallet
501            .fetch_static_deposit_claim_quote(detailed_utxo.tx.clone(), Some(detailed_utxo.vout))
502            .await?;
503        let spark_requested_fee = detailed_utxo.value.saturating_sub(quote.credit_amount_sats);
504        let Some(max_deposit_claim_fee) = max_claim_fee else {
505            return Err(SdkError::DepositClaimFeeExceeded {
506                tx: detailed_utxo.txid.to_string(),
507                vout: detailed_utxo.vout,
508                max_fee: None,
509                actual_fee: spark_requested_fee,
510            });
511        };
512        match max_deposit_claim_fee {
513            Fee::Fixed { amount } => {
514                info!(
515                    "User max fee: {} spark requested fee: {}",
516                    amount, spark_requested_fee
517                );
518                if spark_requested_fee > amount {
519                    return Err(SdkError::DepositClaimFeeExceeded {
520                        tx: detailed_utxo.txid.to_string(),
521                        vout: detailed_utxo.vout,
522                        max_fee: Some(max_deposit_claim_fee),
523                        actual_fee: spark_requested_fee,
524                    });
525                }
526            }
527            Fee::Rate { sat_per_vbyte } => {
528                // The claim tx size is 99 vbytes
529                const CLAIM_TX_SIZE: u64 = 99;
530                let user_max_fee = CLAIM_TX_SIZE.saturating_mul(sat_per_vbyte);
531                info!(
532                    "User max fee: {} spark requested fee: {}",
533                    user_max_fee, spark_requested_fee
534                );
535                if spark_requested_fee > user_max_fee {
536                    return Err(SdkError::DepositClaimFeeExceeded {
537                        tx: detailed_utxo.txid.to_string(),
538                        vout: detailed_utxo.vout,
539                        max_fee: Some(max_deposit_claim_fee),
540                        actual_fee: spark_requested_fee,
541                    });
542                }
543            }
544        }
545        info!(
546            "Claiming static deposit for utxo {}:{}",
547            detailed_utxo.txid, detailed_utxo.vout
548        );
549        let transfer = self.spark_wallet.claim_static_deposit(quote).await?;
550        info!(
551            "Claimed static deposit transfer: {}",
552            serde_json::to_string_pretty(&transfer)?
553        );
554        Ok(transfer)
555    }
556
557    async fn ensure_spark_private_mode_initialized(&self) -> Result<(), SdkError> {
558        self.spark_private_mode_initialized
559            .get_or_try_init(|| async {
560                // Check if already initialized in storage
561                let object_repository = ObjectCacheRepository::new(self.storage.clone());
562                let is_initialized = object_repository
563                    .fetch_spark_private_mode_initialized()
564                    .await?;
565
566                if !is_initialized {
567                    // Initialize if not already done
568                    self.initialize_spark_private_mode().await?;
569                }
570                Ok::<_, SdkError>(())
571            })
572            .await?;
573        Ok(())
574    }
575
576    async fn initialize_spark_private_mode(&self) -> Result<(), SdkError> {
577        if !self.config.private_enabled_default {
578            ObjectCacheRepository::new(self.storage.clone())
579                .save_spark_private_mode_initialized()
580                .await?;
581            info!("Spark private mode initialized: no changes needed");
582            return Ok(());
583        }
584
585        // Enable spark private mode
586        self.update_user_settings(UpdateUserSettingsRequest {
587            spark_private_mode_enabled: Some(true),
588        })
589        .await?;
590        ObjectCacheRepository::new(self.storage.clone())
591            .save_spark_private_mode_initialized()
592            .await?;
593        info!("Spark private mode initialized: enabled");
594        Ok(())
595    }
596}
597
598#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
599#[allow(clippy::needless_pass_by_value)]
600impl BreezSdk {
601    /// Registers a listener to receive SDK events
602    ///
603    /// # Arguments
604    ///
605    /// * `listener` - An implementation of the `EventListener` trait
606    ///
607    /// # Returns
608    ///
609    /// A unique identifier for the listener, which can be used to remove it later
610    pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
611        self.event_emitter.add_listener(listener).await
612    }
613
614    /// Removes a previously registered event listener
615    ///
616    /// # Arguments
617    ///
618    /// * `id` - The listener ID returned from `add_event_listener`
619    ///
620    /// # Returns
621    ///
622    /// `true` if the listener was found and removed, `false` otherwise
623    pub async fn remove_event_listener(&self, id: &str) -> bool {
624        self.event_emitter.remove_listener(id).await
625    }
626
627    /// Stops the SDK's background tasks
628    ///
629    /// This method stops the background tasks started by the `start()` method.
630    /// It should be called before your application terminates to ensure proper cleanup.
631    ///
632    /// # Returns
633    ///
634    /// Result containing either success or an `SdkError` if the background task couldn't be stopped
635    pub async fn disconnect(&self) -> Result<(), SdkError> {
636        info!("Disconnecting Breez SDK");
637        self.shutdown_sender
638            .send(())
639            .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
640
641        self.shutdown_sender.closed().await;
642        info!("Breez SDK disconnected");
643        Ok(())
644    }
645
646    pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
647        parse_input(input, Some(self.external_input_parsers.clone())).await
648    }
649
650    /// Returns the balance of the wallet in satoshis
651    #[allow(unused_variables)]
652    pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
653        if request.ensure_synced.unwrap_or_default() {
654            self.initial_synced_watcher
655                .clone()
656                .changed()
657                .await
658                .map_err(|_| {
659                    SdkError::Generic("Failed to receive initial synced signal".to_string())
660                })?;
661        }
662        let object_repository = ObjectCacheRepository::new(self.storage.clone());
663        let account_info = object_repository
664            .fetch_account_info()
665            .await?
666            .unwrap_or_default();
667        Ok(GetInfoResponse {
668            balance_sats: account_info.balance_sats,
669            token_balances: account_info.token_balances,
670        })
671    }
672
673    pub async fn receive_payment(
674        &self,
675        request: ReceivePaymentRequest,
676    ) -> Result<ReceivePaymentResponse, SdkError> {
677        self.ensure_spark_private_mode_initialized().await?;
678        match request.payment_method {
679            ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse {
680                fee: 0,
681                payment_request: self
682                    .spark_wallet
683                    .get_spark_address()?
684                    .to_address_string()
685                    .map_err(|e| {
686                        SdkError::Generic(format!("Failed to convert Spark address to string: {e}"))
687                    })?,
688            }),
689            ReceivePaymentMethod::SparkInvoice {
690                amount,
691                token_identifier,
692                expiry_time,
693                description,
694                sender_public_key,
695            } => {
696                let invoice = self.spark_wallet.create_spark_invoice(
697                    amount,
698                    token_identifier.clone(),
699                    expiry_time
700                        .map(|time| {
701                            SystemTime::UNIX_EPOCH
702                                .checked_add(Duration::from_secs(time))
703                                .ok_or(SdkError::Generic("Invalid expiry time".to_string()))
704                        })
705                        .transpose()?,
706                    description,
707                    sender_public_key.map(|key| PublicKey::from_str(&key).unwrap()),
708                )?;
709                Ok(ReceivePaymentResponse {
710                    fee: 0,
711                    payment_request: invoice,
712                })
713            }
714            ReceivePaymentMethod::BitcoinAddress => {
715                // TODO: allow passing amount
716
717                let object_repository = ObjectCacheRepository::new(self.storage.clone());
718
719                // First lookup in storage cache
720                let static_deposit_address =
721                    object_repository.fetch_static_deposit_address().await?;
722                if let Some(static_deposit_address) = static_deposit_address {
723                    return Ok(ReceivePaymentResponse {
724                        payment_request: static_deposit_address.address.clone(),
725                        fee: 0,
726                    });
727                }
728
729                // Then query existing addresses
730                let deposit_addresses = self
731                    .spark_wallet
732                    .list_static_deposit_addresses(None)
733                    .await?;
734
735                // In case there are no addresses, generate a new one and cache it
736                let address = match deposit_addresses.items.last() {
737                    Some(address) => address.to_string(),
738                    None => self
739                        .spark_wallet
740                        .generate_deposit_address(true)
741                        .await?
742                        .to_string(),
743                };
744
745                object_repository
746                    .save_static_deposit_address(&StaticDepositAddress {
747                        address: address.clone(),
748                    })
749                    .await?;
750
751                Ok(ReceivePaymentResponse {
752                    payment_request: address,
753                    fee: 0,
754                })
755            }
756            ReceivePaymentMethod::Bolt11Invoice {
757                description,
758                amount_sats,
759            } => Ok(ReceivePaymentResponse {
760                payment_request: self
761                    .spark_wallet
762                    .create_lightning_invoice(
763                        amount_sats.unwrap_or_default(),
764                        Some(InvoiceDescription::Memo(description.clone())),
765                        None,
766                        self.config.prefer_spark_over_lightning,
767                    )
768                    .await?
769                    .invoice,
770                fee: 0,
771            }),
772        }
773    }
774
775    pub async fn prepare_lnurl_pay(
776        &self,
777        request: PrepareLnurlPayRequest,
778    ) -> Result<PrepareLnurlPayResponse, SdkError> {
779        let success_data = match validate_lnurl_pay(
780            self.lnurl_client.as_ref(),
781            request.amount_sats.saturating_mul(1_000),
782            &None,
783            &request.pay_request.clone().into(),
784            self.config.network.into(),
785            request.validate_success_action_url,
786        )
787        .await?
788        {
789            lnurl::pay::ValidatedCallbackResponse::EndpointError { data } => {
790                return Err(LnurlError::EndpointError(data.reason).into());
791            }
792            lnurl::pay::ValidatedCallbackResponse::EndpointSuccess { data } => data,
793        };
794
795        let prepare_response = self
796            .prepare_send_payment(PrepareSendPaymentRequest {
797                payment_request: success_data.pr,
798                amount: Some(request.amount_sats.into()),
799                token_identifier: None,
800            })
801            .await?;
802
803        let SendPaymentMethod::Bolt11Invoice {
804            invoice_details,
805            lightning_fee_sats,
806            ..
807        } = prepare_response.payment_method
808        else {
809            return Err(SdkError::Generic(
810                "Expected Bolt11Invoice payment method".to_string(),
811            ));
812        };
813
814        Ok(PrepareLnurlPayResponse {
815            amount_sats: request.amount_sats,
816            comment: request.comment,
817            pay_request: request.pay_request,
818            invoice_details,
819            fee_sats: lightning_fee_sats,
820            success_action: success_data.success_action.map(From::from),
821        })
822    }
823
824    pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
825        self.ensure_spark_private_mode_initialized().await?;
826        let mut payment = Box::pin(self.send_payment_internal(
827            SendPaymentRequest {
828                prepare_response: PrepareSendPaymentResponse {
829                    payment_method: SendPaymentMethod::Bolt11Invoice {
830                        invoice_details: request.prepare_response.invoice_details,
831                        spark_transfer_fee_sats: None,
832                        lightning_fee_sats: request.prepare_response.fee_sats,
833                    },
834                    amount: request.prepare_response.amount_sats.into(),
835                    token_identifier: None,
836                },
837                options: None,
838            },
839            true,
840        ))
841        .await?
842        .payment;
843
844        let success_action = process_success_action(
845            &payment,
846            request
847                .prepare_response
848                .success_action
849                .clone()
850                .map(Into::into)
851                .as_ref(),
852        )?;
853
854        let lnurl_info = LnurlPayInfo {
855            ln_address: request.prepare_response.pay_request.address,
856            comment: request.prepare_response.comment,
857            domain: Some(request.prepare_response.pay_request.domain),
858            metadata: Some(request.prepare_response.pay_request.metadata_str),
859            processed_success_action: success_action.clone().map(From::from),
860            raw_success_action: request.prepare_response.success_action,
861        };
862        let Some(PaymentDetails::Lightning {
863            lnurl_pay_info,
864            description,
865            ..
866        }) = &mut payment.details
867        else {
868            return Err(SdkError::Generic(
869                "Expected Lightning payment details".to_string(),
870            ));
871        };
872        *lnurl_pay_info = Some(lnurl_info.clone());
873
874        let lnurl_description = lnurl_info.extract_description();
875        description.clone_from(&lnurl_description);
876
877        self.storage
878            .set_payment_metadata(
879                payment.id.clone(),
880                PaymentMetadata {
881                    lnurl_pay_info: Some(lnurl_info),
882                    lnurl_description,
883                    ..Default::default()
884                },
885            )
886            .await?;
887
888        emit_payment_status(&self.event_emitter, payment.clone()).await;
889        Ok(LnurlPayResponse {
890            payment,
891            success_action: success_action.map(From::from),
892        })
893    }
894
895    /// Performs an LNURL withdraw operation for the amount of satoshis to
896    /// withdraw and the LNURL withdraw request details. The LNURL withdraw request
897    /// details can be obtained from calling [`BreezSdk::parse`].
898    ///
899    /// The method generates a Lightning invoice for the withdraw amount, stores
900    /// the LNURL withdraw metadata, and performs the LNURL withdraw using  the generated
901    /// invoice.
902    ///
903    /// If the `completion_timeout_secs` parameter is provided and greater than 0, the
904    /// method will wait for the payment to be completed within that period. If the
905    /// withdraw is completed within the timeout, the `payment` field in the response
906    /// will be set with the payment details. If the `completion_timeout_secs`
907    /// parameter is not provided or set to 0, the method will not wait for the payment
908    /// to be completed. If the withdraw is not completed within the
909    /// timeout, the `payment` field will be empty.
910    ///
911    /// # Arguments
912    ///
913    /// * `request` - The LNURL withdraw request
914    ///
915    /// # Returns
916    ///
917    /// Result containing either:
918    /// * `LnurlWithdrawResponse` - The payment details if the withdraw request was successful
919    /// * `SdkError` - If there was an error during the withdraw process
920    pub async fn lnurl_withdraw(
921        &self,
922        request: LnurlWithdrawRequest,
923    ) -> Result<LnurlWithdrawResponse, SdkError> {
924        self.ensure_spark_private_mode_initialized().await?;
925        let LnurlWithdrawRequest {
926            amount_sats,
927            withdraw_request,
928            completion_timeout_secs,
929        } = request;
930        let withdraw_request: breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails =
931            withdraw_request.into();
932        if !withdraw_request.is_amount_valid(amount_sats) {
933            return Err(SdkError::InvalidInput(
934                "Amount must be within min/max LNURL withdrawable limits".to_string(),
935            ));
936        }
937
938        // Generate a Lightning invoice for the withdraw
939        let payment_request = self
940            .receive_payment(ReceivePaymentRequest {
941                payment_method: ReceivePaymentMethod::Bolt11Invoice {
942                    description: withdraw_request.default_description.clone(),
943                    amount_sats: Some(amount_sats),
944                },
945            })
946            .await?
947            .payment_request;
948
949        // Store the LNURL withdraw metadata before executing the withdraw
950        let cache = ObjectCacheRepository::new(self.storage.clone());
951        cache
952            .save_payment_request_metadata(&PaymentRequestMetadata {
953                payment_request: payment_request.clone(),
954                lnurl_withdraw_request_details: withdraw_request.clone(),
955            })
956            .await?;
957
958        // Perform the LNURL withdraw using the generated invoice
959        let withdraw_response = execute_lnurl_withdraw(
960            self.lnurl_client.as_ref(),
961            &withdraw_request,
962            &payment_request,
963        )
964        .await?;
965        if let lnurl::withdraw::ValidatedCallbackResponse::EndpointError { data } =
966            withdraw_response
967        {
968            return Err(LnurlError::EndpointError(data.reason).into());
969        }
970
971        let completion_timeout_secs = match completion_timeout_secs {
972            Some(secs) if secs > 0 => secs,
973            _ => {
974                return Ok(LnurlWithdrawResponse {
975                    payment_request,
976                    payment: None,
977                });
978            }
979        };
980
981        // Wait for the payment to be completed
982        let fut = self.wait_for_payment(WaitForPaymentRequest {
983            identifier: WaitForPaymentIdentifier::PaymentRequest(payment_request.clone()),
984        });
985
986        let payment = match timeout(Duration::from_secs(completion_timeout_secs.into()), fut).await
987        {
988            // Payment completed successfully
989            Ok(Ok(res)) => Some(res.payment),
990            // Error occurred while waiting for payment
991            Ok(Err(e)) => return Err(SdkError::Generic(format!("Error waiting for payment: {e}"))),
992            // Timeout occurred
993            Err(_) => None,
994        };
995
996        Ok(LnurlWithdrawResponse {
997            payment_request,
998            payment,
999        })
1000    }
1001
1002    #[allow(clippy::too_many_lines)]
1003    pub async fn prepare_send_payment(
1004        &self,
1005        request: PrepareSendPaymentRequest,
1006    ) -> Result<PrepareSendPaymentResponse, SdkError> {
1007        let parsed_input = self.parse(&request.payment_request).await?;
1008
1009        validate_prepare_send_payment_request(
1010            &parsed_input,
1011            &request,
1012            &self.spark_wallet.get_identity_public_key().to_string(),
1013        )?;
1014
1015        match &parsed_input {
1016            InputType::SparkAddress(spark_address_details) => Ok(PrepareSendPaymentResponse {
1017                payment_method: SendPaymentMethod::SparkAddress {
1018                    address: spark_address_details.address.clone(),
1019                    fee: 0,
1020                    token_identifier: request.token_identifier.clone(),
1021                },
1022                amount: request
1023                    .amount
1024                    .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1025                token_identifier: request.token_identifier,
1026            }),
1027            InputType::SparkInvoice(spark_invoice_details) => Ok(PrepareSendPaymentResponse {
1028                payment_method: SendPaymentMethod::SparkInvoice {
1029                    spark_invoice_details: spark_invoice_details.clone(),
1030                    fee: 0,
1031                    token_identifier: request.token_identifier.clone(),
1032                },
1033                amount: spark_invoice_details
1034                    .amount
1035                    .or(request.amount)
1036                    .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1037                token_identifier: request.token_identifier,
1038            }),
1039            InputType::Bolt11Invoice(detailed_bolt11_invoice) => {
1040                let spark_address = self
1041                    .spark_wallet
1042                    .extract_spark_address(&request.payment_request)?;
1043
1044                let spark_transfer_fee_sats = if spark_address.is_some() {
1045                    Some(0)
1046                } else {
1047                    None
1048                };
1049
1050                let lightning_fee_sats = self
1051                    .spark_wallet
1052                    .fetch_lightning_send_fee_estimate(
1053                        &request.payment_request,
1054                        request
1055                            .amount
1056                            .map(|a| Ok::<u64, SdkError>(a.try_into()?))
1057                            .transpose()?,
1058                    )
1059                    .await?;
1060
1061                Ok(PrepareSendPaymentResponse {
1062                    payment_method: SendPaymentMethod::Bolt11Invoice {
1063                        invoice_details: detailed_bolt11_invoice.clone(),
1064                        spark_transfer_fee_sats,
1065                        lightning_fee_sats,
1066                    },
1067                    amount: request
1068                        .amount
1069                        .or(detailed_bolt11_invoice
1070                            .amount_msat
1071                            .map(|msat| u128::from(msat) / 1000))
1072                        .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1073                    token_identifier: None,
1074                })
1075            }
1076            InputType::BitcoinAddress(withdrawal_address) => {
1077                let fee_quote = self
1078                    .spark_wallet
1079                    .fetch_coop_exit_fee_quote(
1080                        &withdrawal_address.address,
1081                        Some(
1082                            request
1083                                .amount
1084                                .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?
1085                                .try_into()?,
1086                        ),
1087                    )
1088                    .await?;
1089                Ok(PrepareSendPaymentResponse {
1090                    payment_method: SendPaymentMethod::BitcoinAddress {
1091                        address: withdrawal_address.clone(),
1092                        fee_quote: fee_quote.into(),
1093                    },
1094                    amount: request
1095                        .amount
1096                        .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1097                    token_identifier: None,
1098                })
1099            }
1100            _ => Err(SdkError::InvalidInput(
1101                "Unsupported payment method".to_string(),
1102            )),
1103        }
1104    }
1105
1106    pub async fn send_payment(
1107        &self,
1108        request: SendPaymentRequest,
1109    ) -> Result<SendPaymentResponse, SdkError> {
1110        self.ensure_spark_private_mode_initialized().await?;
1111        Box::pin(self.send_payment_internal(request, false)).await
1112    }
1113
1114    /// Synchronizes the wallet with the Spark network
1115    #[allow(unused_variables)]
1116    pub async fn sync_wallet(
1117        &self,
1118        request: SyncWalletRequest,
1119    ) -> Result<SyncWalletResponse, SdkError> {
1120        let (tx, rx) = oneshot::channel();
1121
1122        if let Err(e) = self.sync_trigger.send(SyncRequest::full(Some(tx))) {
1123            error!("Failed to send sync trigger: {e:?}");
1124        }
1125        let _ = rx.await.map_err(|e| {
1126            error!("Failed to receive sync trigger: {e:?}");
1127            SdkError::Generic(format!("sync trigger failed: {e:?}"))
1128        })?;
1129        Ok(SyncWalletResponse {})
1130    }
1131
1132    /// Lists payments from the storage with pagination
1133    ///
1134    /// This method provides direct access to the payment history stored in the database.
1135    /// It returns payments in reverse chronological order (newest first).
1136    ///
1137    /// # Arguments
1138    ///
1139    /// * `request` - Contains pagination parameters (offset and limit)
1140    ///
1141    /// # Returns
1142    ///
1143    /// * `Ok(ListPaymentsResponse)` - Contains the list of payments if successful
1144    /// * `Err(SdkError)` - If there was an error accessing the storage
1145    ///
1146    pub async fn list_payments(
1147        &self,
1148        request: ListPaymentsRequest,
1149    ) -> Result<ListPaymentsResponse, SdkError> {
1150        let payments = self.storage.list_payments(request).await?;
1151        Ok(ListPaymentsResponse { payments })
1152    }
1153
1154    pub async fn get_payment(
1155        &self,
1156        request: GetPaymentRequest,
1157    ) -> Result<GetPaymentResponse, SdkError> {
1158        let payment = self.storage.get_payment_by_id(request.payment_id).await?;
1159        Ok(GetPaymentResponse { payment })
1160    }
1161
1162    pub async fn claim_deposit(
1163        &self,
1164        request: ClaimDepositRequest,
1165    ) -> Result<ClaimDepositResponse, SdkError> {
1166        self.ensure_spark_private_mode_initialized().await?;
1167        let detailed_utxo =
1168            CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1169                .fetch_detailed_utxo(&request.txid, request.vout)
1170                .await?;
1171
1172        let max_fee = request
1173            .max_fee
1174            .or(self.config.max_deposit_claim_fee.clone());
1175        match self.claim_utxo(&detailed_utxo, max_fee).await {
1176            Ok(transfer) => {
1177                self.storage
1178                    .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
1179                    .await?;
1180                if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
1181                    error!("Failed to execute sync after deposit claim: {e:?}");
1182                }
1183                Ok(ClaimDepositResponse {
1184                    payment: transfer.try_into()?,
1185                })
1186            }
1187            Err(e) => {
1188                error!("Failed to claim deposit: {e:?}");
1189                self.storage
1190                    .update_deposit(
1191                        detailed_utxo.txid.to_string(),
1192                        detailed_utxo.vout,
1193                        UpdateDepositPayload::ClaimError {
1194                            error: e.clone().into(),
1195                        },
1196                    )
1197                    .await?;
1198                Err(e)
1199            }
1200        }
1201    }
1202
1203    pub async fn refund_deposit(
1204        &self,
1205        request: RefundDepositRequest,
1206    ) -> Result<RefundDepositResponse, SdkError> {
1207        let detailed_utxo =
1208            CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1209                .fetch_detailed_utxo(&request.txid, request.vout)
1210                .await?;
1211        let tx = self
1212            .spark_wallet
1213            .refund_static_deposit(
1214                detailed_utxo.clone().tx,
1215                Some(detailed_utxo.vout),
1216                &request.destination_address,
1217                request.fee.into(),
1218            )
1219            .await?;
1220        let deposit: DepositInfo = detailed_utxo.into();
1221        let tx_hex = serialize(&tx).as_hex().to_string();
1222        let tx_id = tx.compute_txid().as_raw_hash().to_string();
1223
1224        // Store the refund transaction details separately
1225        self.storage
1226            .update_deposit(
1227                deposit.txid.clone(),
1228                deposit.vout,
1229                UpdateDepositPayload::Refund {
1230                    refund_tx: tx_hex.clone(),
1231                    refund_txid: tx_id.clone(),
1232                },
1233            )
1234            .await?;
1235
1236        self.chain_service
1237            .broadcast_transaction(tx_hex.clone())
1238            .await?;
1239        Ok(RefundDepositResponse { tx_id, tx_hex })
1240    }
1241
1242    #[allow(unused_variables)]
1243    pub async fn list_unclaimed_deposits(
1244        &self,
1245        request: ListUnclaimedDepositsRequest,
1246    ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
1247        let deposits = self.storage.list_deposits().await?;
1248        Ok(ListUnclaimedDepositsResponse { deposits })
1249    }
1250
1251    pub async fn check_lightning_address_available(
1252        &self,
1253        req: CheckLightningAddressRequest,
1254    ) -> Result<bool, SdkError> {
1255        let Some(client) = &self.lnurl_server_client else {
1256            return Err(SdkError::Generic(
1257                "LNURL server is not configured".to_string(),
1258            ));
1259        };
1260
1261        let username = sanitize_username(&req.username);
1262        let available = client.check_username_available(&username).await?;
1263        Ok(available)
1264    }
1265
1266    pub async fn get_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
1267        let cache = ObjectCacheRepository::new(self.storage.clone());
1268        Ok(cache.fetch_lightning_address().await?)
1269    }
1270
1271    pub async fn register_lightning_address(
1272        &self,
1273        request: RegisterLightningAddressRequest,
1274    ) -> Result<LightningAddressInfo, SdkError> {
1275        let cache = ObjectCacheRepository::new(self.storage.clone());
1276        let Some(client) = &self.lnurl_server_client else {
1277            return Err(SdkError::Generic(
1278                "LNURL server is not configured".to_string(),
1279            ));
1280        };
1281
1282        let username = sanitize_username(&request.username);
1283
1284        let description = match request.description {
1285            Some(description) => description,
1286            None => format!("Pay to {}@{}", username, client.domain()),
1287        };
1288        let params = crate::lnurl::RegisterLightningAddressRequest {
1289            username: username.clone(),
1290            description: description.clone(),
1291        };
1292
1293        let response = client.register_lightning_address(&params).await?;
1294        let address_info = LightningAddressInfo {
1295            lightning_address: response.lightning_address,
1296            description,
1297            lnurl: response.lnurl,
1298            username,
1299        };
1300        cache.save_lightning_address(&address_info).await?;
1301        Ok(address_info)
1302    }
1303
1304    pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
1305        let cache = ObjectCacheRepository::new(self.storage.clone());
1306        let Some(address_info) = cache.fetch_lightning_address().await? else {
1307            return Ok(());
1308        };
1309
1310        let Some(client) = &self.lnurl_server_client else {
1311            return Err(SdkError::Generic(
1312                "LNURL server is not configured".to_string(),
1313            ));
1314        };
1315
1316        let params = crate::lnurl::UnregisterLightningAddressRequest {
1317            username: address_info.username,
1318        };
1319
1320        client.unregister_lightning_address(&params).await?;
1321        cache.delete_lightning_address().await?;
1322        Ok(())
1323    }
1324
1325    /// List fiat currencies for which there is a known exchange rate,
1326    /// sorted by the canonical name of the currency.
1327    pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
1328        let currencies = self
1329            .fiat_service
1330            .fetch_fiat_currencies()
1331            .await?
1332            .into_iter()
1333            .map(From::from)
1334            .collect();
1335        Ok(ListFiatCurrenciesResponse { currencies })
1336    }
1337
1338    /// List the latest rates of fiat currencies, sorted by name.
1339    pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
1340        let rates = self
1341            .fiat_service
1342            .fetch_fiat_rates()
1343            .await?
1344            .into_iter()
1345            .map(From::from)
1346            .collect();
1347        Ok(ListFiatRatesResponse { rates })
1348    }
1349
1350    pub async fn wait_for_payment(
1351        &self,
1352        request: WaitForPaymentRequest,
1353    ) -> Result<WaitForPaymentResponse, SdkError> {
1354        let (tx, mut rx) = mpsc::channel(20);
1355        let id = self
1356            .add_event_listener(Box::new(InternalEventListener::new(tx)))
1357            .await;
1358
1359        // First check if we already have the payment in storage
1360        if let WaitForPaymentIdentifier::PaymentRequest(payment_request) = &request.identifier
1361            && let Some(payment) = self
1362                .storage
1363                .get_payment_by_invoice(payment_request.clone())
1364                .await?
1365        {
1366            self.remove_event_listener(&id).await;
1367            return Ok(WaitForPaymentResponse { payment });
1368        }
1369
1370        // Otherwise, we wait for a matching payment event
1371        let payment_result = loop {
1372            let Some(event) = rx.recv().await else {
1373                break Err(SdkError::Generic("Event channel closed".to_string()));
1374            };
1375
1376            let SdkEvent::PaymentSucceeded { payment } = event else {
1377                continue;
1378            };
1379
1380            if is_payment_match(&payment, &request) {
1381                break Ok(payment);
1382            }
1383        };
1384
1385        self.remove_event_listener(&id).await;
1386        Ok(WaitForPaymentResponse {
1387            payment: payment_result?,
1388        })
1389    }
1390
1391    /// Returns the metadata for the given token identifiers.
1392    ///
1393    /// Results are not guaranteed to be in the same order as the input token identifiers.    
1394    ///
1395    /// If the metadata is not found locally in cache, it will be queried from
1396    /// the Spark network and then cached.
1397    pub async fn get_tokens_metadata(
1398        &self,
1399        request: GetTokensMetadataRequest,
1400    ) -> Result<GetTokensMetadataResponse, SdkError> {
1401        let metadata = get_tokens_metadata_cached_or_query(
1402            &self.spark_wallet,
1403            &ObjectCacheRepository::new(self.storage.clone()),
1404            &request
1405                .token_identifiers
1406                .iter()
1407                .map(String::as_str)
1408                .collect::<Vec<_>>(),
1409        )
1410        .await?;
1411        Ok(GetTokensMetadataResponse {
1412            tokens_metadata: metadata,
1413        })
1414    }
1415
1416    /// Signs a message with the wallet's identity key. The message is SHA256
1417    /// hashed before signing. The returned signature will be hex encoded in
1418    /// DER format by default, or compact format if specified.
1419    pub async fn sign_message(
1420        &self,
1421        request: SignMessageRequest,
1422    ) -> Result<SignMessageResponse, SdkError> {
1423        let pubkey = self.spark_wallet.get_identity_public_key().to_string();
1424        let signature = self.spark_wallet.sign_message(&request.message).await?;
1425        let signature_hex = if request.compact {
1426            signature.serialize_compact().to_lower_hex_string()
1427        } else {
1428            signature.serialize_der().to_lower_hex_string()
1429        };
1430
1431        Ok(SignMessageResponse {
1432            pubkey,
1433            signature: signature_hex,
1434        })
1435    }
1436
1437    /// Verifies a message signature against the provided public key. The message
1438    /// is SHA256 hashed before verification. The signature can be hex encoded
1439    /// in either DER or compact format.
1440    pub async fn check_message(
1441        &self,
1442        request: CheckMessageRequest,
1443    ) -> Result<CheckMessageResponse, SdkError> {
1444        let pubkey = PublicKey::from_str(&request.pubkey)
1445            .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
1446        let signature_bytes = hex::decode(&request.signature)
1447            .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
1448        let signature = Signature::from_der(&signature_bytes)
1449            .or_else(|_| Signature::from_compact(&signature_bytes))
1450            .map_err(|_| {
1451                SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
1452            })?;
1453
1454        let is_valid = self
1455            .spark_wallet
1456            .verify_message(&request.message, &signature, &pubkey)
1457            .await
1458            .is_ok();
1459        Ok(CheckMessageResponse { is_valid })
1460    }
1461
1462    /// Returns the user settings for the wallet.
1463    ///
1464    /// Some settings are fetched from the Spark network so network requests are performed.
1465    pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
1466        // Ensure spark private mode is initialized to avoid race conditions with the initialization task.
1467        self.ensure_spark_private_mode_initialized().await?;
1468
1469        let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
1470
1471        // We may in the future have user settings that are stored locally and synced using real-time sync.
1472
1473        Ok(UserSettings {
1474            spark_private_mode_enabled: spark_user_settings.private_enabled,
1475        })
1476    }
1477
1478    /// Updates the user settings for the wallet.
1479    ///
1480    /// Some settings are updated on the Spark network so network requests may be performed.
1481    pub async fn update_user_settings(
1482        &self,
1483        request: UpdateUserSettingsRequest,
1484    ) -> Result<(), SdkError> {
1485        if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
1486            self.spark_wallet
1487                .update_wallet_settings(spark_private_mode_enabled)
1488                .await?;
1489        }
1490        Ok(())
1491    }
1492
1493    /// Returns an instance of the [`TokenIssuer`] for managing token issuance.
1494    pub fn get_token_issuer(&self) -> TokenIssuer {
1495        TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
1496    }
1497}
1498
1499// Separate impl block to avoid exposing private methods to uniffi.
1500impl BreezSdk {
1501    async fn send_payment_internal(
1502        &self,
1503        request: SendPaymentRequest,
1504        suppress_payment_event: bool,
1505    ) -> Result<SendPaymentResponse, SdkError> {
1506        let res = match &request.prepare_response.payment_method {
1507            SendPaymentMethod::SparkAddress {
1508                address,
1509                token_identifier,
1510                ..
1511            } => {
1512                self.send_spark_address(address, token_identifier.clone(), &request)
1513                    .await
1514            }
1515            SendPaymentMethod::SparkInvoice {
1516                spark_invoice_details,
1517                ..
1518            } => {
1519                self.send_spark_invoice(&spark_invoice_details.invoice, &request)
1520                    .await
1521            }
1522            SendPaymentMethod::Bolt11Invoice {
1523                invoice_details,
1524                spark_transfer_fee_sats,
1525                lightning_fee_sats,
1526            } => {
1527                self.send_bolt11_invoice(
1528                    invoice_details,
1529                    *spark_transfer_fee_sats,
1530                    *lightning_fee_sats,
1531                    &request,
1532                )
1533                .await
1534            }
1535            SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
1536                self.send_bitcoin_address(address, fee_quote, &request)
1537                    .await
1538            }
1539        };
1540        if let Ok(response) = &res {
1541            //TODO: We get incomplete payments here from the ssp so better not to persist for now.
1542            // we trigger the sync here anyway to get the fresh payment.
1543            //self.storage.insert_payment(response.payment.clone()).await?;
1544            if !suppress_payment_event {
1545                emit_payment_status(&self.event_emitter, response.payment.clone()).await;
1546            }
1547            if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
1548                error!("Failed to send sync trigger: {e:?}");
1549            }
1550        }
1551        res
1552    }
1553
1554    async fn send_spark_address(
1555        &self,
1556        address: &str,
1557        token_identifier: Option<String>,
1558        request: &SendPaymentRequest,
1559    ) -> Result<SendPaymentResponse, SdkError> {
1560        let spark_address = address
1561            .parse::<SparkAddress>()
1562            .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
1563
1564        let payment = if let Some(identifier) = token_identifier {
1565            self.send_spark_token_address(
1566                identifier,
1567                request.prepare_response.amount,
1568                spark_address,
1569            )
1570            .await?
1571        } else {
1572            let transfer = self
1573                .spark_wallet
1574                .transfer(request.prepare_response.amount.try_into()?, &spark_address)
1575                .await?;
1576            transfer.try_into()?
1577        };
1578
1579        Ok(SendPaymentResponse { payment })
1580    }
1581
1582    async fn send_spark_token_address(
1583        &self,
1584        token_identifier: String,
1585        amount: u128,
1586        receiver_address: SparkAddress,
1587    ) -> Result<Payment, SdkError> {
1588        let token_transaction = self
1589            .spark_wallet
1590            .transfer_tokens(
1591                vec![TransferTokenOutput {
1592                    token_id: token_identifier,
1593                    amount,
1594                    receiver_address: receiver_address.clone(),
1595                    spark_invoice: None,
1596                }],
1597                None,
1598            )
1599            .await?;
1600
1601        map_and_persist_token_transaction(&self.spark_wallet, &self.storage, &token_transaction)
1602            .await
1603    }
1604
1605    async fn send_spark_invoice(
1606        &self,
1607        invoice: &str,
1608        request: &SendPaymentRequest,
1609    ) -> Result<SendPaymentResponse, SdkError> {
1610        let payment = match self
1611            .spark_wallet
1612            .fulfill_spark_invoice(invoice, Some(request.prepare_response.amount))
1613            .await?
1614        {
1615            spark_wallet::FulfillSparkInvoiceResult::Transfer(wallet_transfer) => {
1616                (*wallet_transfer).try_into()?
1617            }
1618            spark_wallet::FulfillSparkInvoiceResult::TokenTransaction(token_transaction) => {
1619                map_and_persist_token_transaction(
1620                    &self.spark_wallet,
1621                    &self.storage,
1622                    &token_transaction,
1623                )
1624                .await?
1625            }
1626        };
1627
1628        Ok(SendPaymentResponse { payment })
1629    }
1630
1631    async fn send_bolt11_invoice(
1632        &self,
1633        invoice_details: &Bolt11InvoiceDetails,
1634        spark_transfer_fee_sats: Option<u64>,
1635        lightning_fee_sats: u64,
1636        request: &SendPaymentRequest,
1637    ) -> Result<SendPaymentResponse, SdkError> {
1638        let amount_to_send = match invoice_details.amount_msat {
1639            // We are not sending amount in case the invoice contains it.
1640            Some(_) => None,
1641            // We are sending amount for zero amount invoice
1642            None => Some(request.prepare_response.amount),
1643        };
1644        let (prefer_spark, completion_timeout_secs) = match request.options {
1645            Some(SendPaymentOptions::Bolt11Invoice {
1646                prefer_spark,
1647                completion_timeout_secs,
1648            }) => (prefer_spark, completion_timeout_secs),
1649            _ => (self.config.prefer_spark_over_lightning, None),
1650        };
1651        let fee_sats = match (prefer_spark, spark_transfer_fee_sats, lightning_fee_sats) {
1652            (true, Some(fee), _) => fee,
1653            _ => lightning_fee_sats,
1654        };
1655
1656        let payment_response = self
1657            .spark_wallet
1658            .pay_lightning_invoice(
1659                &invoice_details.invoice.bolt11,
1660                amount_to_send
1661                    .map(|a| Ok::<u64, SdkError>(a.try_into()?))
1662                    .transpose()?,
1663                Some(fee_sats),
1664                prefer_spark,
1665            )
1666            .await?;
1667        let payment = match payment_response.lightning_payment {
1668            Some(lightning_payment) => {
1669                let ssp_id = lightning_payment.id.clone();
1670                let payment = Payment::from_lightning(
1671                    lightning_payment,
1672                    request.prepare_response.amount,
1673                    payment_response.transfer.id.to_string(),
1674                )?;
1675                self.poll_lightning_send_payment(&payment, ssp_id);
1676                payment
1677            }
1678            None => payment_response.transfer.try_into()?,
1679        };
1680
1681        let Some(completion_timeout_secs) = completion_timeout_secs else {
1682            return Ok(SendPaymentResponse { payment });
1683        };
1684
1685        if completion_timeout_secs == 0 {
1686            return Ok(SendPaymentResponse { payment });
1687        }
1688
1689        let fut = self.wait_for_payment(WaitForPaymentRequest {
1690            identifier: WaitForPaymentIdentifier::PaymentId(payment.id.clone()),
1691        });
1692        let payment = match timeout(Duration::from_secs(completion_timeout_secs.into()), fut).await
1693        {
1694            Ok(res) => res?.payment,
1695            // On timeout return the pending payment.
1696            Err(_) => payment,
1697        };
1698
1699        Ok(SendPaymentResponse { payment })
1700    }
1701
1702    async fn send_bitcoin_address(
1703        &self,
1704        address: &BitcoinAddressDetails,
1705        fee_quote: &SendOnchainFeeQuote,
1706        request: &SendPaymentRequest,
1707    ) -> Result<SendPaymentResponse, SdkError> {
1708        let exit_speed: ExitSpeed = match &request.options {
1709            Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) => {
1710                confirmation_speed.clone().into()
1711            }
1712            None => ExitSpeed::Fast,
1713            _ => {
1714                return Err(SdkError::InvalidInput("Invalid options".to_string()));
1715            }
1716        };
1717        let response = self
1718            .spark_wallet
1719            .withdraw(
1720                &address.address,
1721                Some(request.prepare_response.amount.try_into()?),
1722                exit_speed,
1723                fee_quote.clone().into(),
1724            )
1725            .await?;
1726        Ok(SendPaymentResponse {
1727            payment: response.try_into()?,
1728        })
1729    }
1730
1731    // Pools the lightning send payment untill it is in completed state.
1732    fn poll_lightning_send_payment(&self, payment: &Payment, ssp_id: String) {
1733        const MAX_POLL_ATTEMPTS: u32 = 20;
1734        let payment_id = payment.id.clone();
1735        info!("Polling lightning send payment {}", payment_id);
1736
1737        let spark_wallet = self.spark_wallet.clone();
1738        let sync_trigger = self.sync_trigger.clone();
1739        let event_emitter = self.event_emitter.clone();
1740        let payment = payment.clone();
1741        let payment_id = payment_id.clone();
1742        let mut shutdown = self.shutdown_sender.subscribe();
1743
1744        tokio::spawn(async move {
1745            for i in 0..MAX_POLL_ATTEMPTS {
1746                info!(
1747                    "Polling lightning send payment {} attempt {}",
1748                    payment_id, i
1749                );
1750                select! {
1751                  _ = shutdown.changed() => {
1752                    info!("Shutdown signal received");
1753                    return;
1754                  },
1755                    p = spark_wallet.fetch_lightning_send_payment(&ssp_id) => {
1756                      if let Ok(Some(p)) = p && let Ok(payment) = Payment::from_lightning(p.clone(), payment.amount, payment.id.clone()) {
1757                        info!("Polling payment status = {} {:?}", payment.status, p.status);
1758                        if payment.status != PaymentStatus::Pending {
1759                          info!("Polling payment completed status = {}", payment.status);
1760                          emit_payment_status(&event_emitter, payment.clone()).await;
1761                          if let Err(e) = sync_trigger.send(SyncRequest::payments_only(None)) {
1762                            error!("Failed to send sync trigger: {e:?}");
1763                          }
1764                          return;
1765                        }
1766                      }
1767
1768                    let sleep_time = if i < 5 {
1769                        Duration::from_secs(1)
1770                    } else {
1771                        Duration::from_secs(i.into())
1772                    };
1773                    tokio::time::sleep(sleep_time).await;
1774                  }
1775                }
1776            }
1777        });
1778    }
1779
1780    /// Attempts to recover a lightning address from the lnurl server.
1781    async fn recover_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
1782        let cache = ObjectCacheRepository::new(self.storage.clone());
1783
1784        let Some(client) = &self.lnurl_server_client else {
1785            return Err(SdkError::Generic(
1786                "LNURL server is not configured".to_string(),
1787            ));
1788        };
1789        let resp = client.recover_lightning_address().await?;
1790
1791        let result = if let Some(resp) = resp {
1792            let address_info = resp.into();
1793            cache.save_lightning_address(&address_info).await?;
1794            Some(address_info)
1795        } else {
1796            cache.delete_lightning_address().await?;
1797            None
1798        };
1799
1800        Ok(result)
1801    }
1802}
1803
1804fn is_payment_match(payment: &Payment, request: &WaitForPaymentRequest) -> bool {
1805    match &request.identifier {
1806        WaitForPaymentIdentifier::PaymentId(payment_id) => payment.id == *payment_id,
1807        WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
1808            if let Some(details) = &payment.details {
1809                match details {
1810                    PaymentDetails::Lightning { invoice, .. } => {
1811                        invoice.to_lowercase() == payment_request.to_lowercase()
1812                    }
1813                    PaymentDetails::Spark {
1814                        invoice_details: invoice,
1815                    }
1816                    | PaymentDetails::Token {
1817                        invoice_details: invoice,
1818                        ..
1819                    } => {
1820                        if let Some(invoice) = invoice {
1821                            invoice.invoice.to_lowercase() == payment_request.to_lowercase()
1822                        } else {
1823                            false
1824                        }
1825                    }
1826                    PaymentDetails::Withdraw { tx_id: _ }
1827                    | PaymentDetails::Deposit { tx_id: _ } => false,
1828                }
1829            } else {
1830                false
1831            }
1832        }
1833    }
1834}
1835
1836struct BalanceWatcher {
1837    spark_wallet: Arc<SparkWallet>,
1838    storage: Arc<dyn Storage>,
1839}
1840
1841impl BalanceWatcher {
1842    fn new(spark_wallet: Arc<SparkWallet>, storage: Arc<dyn Storage>) -> Self {
1843        Self {
1844            spark_wallet,
1845            storage,
1846        }
1847    }
1848}
1849
1850#[macros::async_trait]
1851impl EventListener for BalanceWatcher {
1852    async fn on_event(&self, event: SdkEvent) {
1853        match event {
1854            SdkEvent::PaymentSucceeded { .. } | SdkEvent::ClaimedDeposits { .. } => {
1855                match update_balances(self.spark_wallet.clone(), self.storage.clone()).await {
1856                    Ok(()) => info!("Balance updated successfully"),
1857                    Err(e) => error!("Failed to update balance: {e:?}"),
1858                }
1859            }
1860            _ => {}
1861        }
1862    }
1863}
1864
1865async fn update_balances(
1866    spark_wallet: Arc<SparkWallet>,
1867    storage: Arc<dyn Storage>,
1868) -> Result<(), SdkError> {
1869    let balance_sats = spark_wallet.get_balance().await?;
1870    let token_balances = spark_wallet
1871        .get_token_balances()
1872        .await?
1873        .into_iter()
1874        .map(|(k, v)| (k, v.into()))
1875        .collect();
1876    let object_repository = ObjectCacheRepository::new(storage.clone());
1877
1878    object_repository
1879        .save_account_info(&CachedAccountInfo {
1880            balance_sats,
1881            token_balances,
1882        })
1883        .await?;
1884    let identity_public_key = spark_wallet.get_identity_public_key();
1885    info!(
1886        "Balance updated successfully {} for identity {}",
1887        balance_sats, identity_public_key
1888    );
1889    Ok(())
1890}
1891
1892struct InternalEventListener {
1893    tx: mpsc::Sender<SdkEvent>,
1894}
1895
1896impl InternalEventListener {
1897    #[allow(unused)]
1898    pub fn new(tx: mpsc::Sender<SdkEvent>) -> Self {
1899        Self { tx }
1900    }
1901}
1902
1903#[macros::async_trait]
1904impl EventListener for InternalEventListener {
1905    async fn on_event(&self, event: SdkEvent) {
1906        let _ = self.tx.send(event).await;
1907    }
1908}
1909
1910fn process_success_action(
1911    payment: &Payment,
1912    success_action: Option<&SuccessAction>,
1913) -> Result<Option<SuccessActionProcessed>, LnurlError> {
1914    let Some(success_action) = success_action else {
1915        return Ok(None);
1916    };
1917
1918    let data = match success_action {
1919        SuccessAction::Aes { data } => data,
1920        SuccessAction::Message { data } => {
1921            return Ok(Some(SuccessActionProcessed::Message { data: data.clone() }));
1922        }
1923        SuccessAction::Url { data } => {
1924            return Ok(Some(SuccessActionProcessed::Url { data: data.clone() }));
1925        }
1926    };
1927
1928    let Some(PaymentDetails::Lightning { preimage, .. }) = &payment.details else {
1929        return Err(LnurlError::general(format!(
1930            "Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.",
1931            payment.details
1932        )));
1933    };
1934
1935    let Some(preimage) = preimage else {
1936        return Ok(None);
1937    };
1938
1939    let preimage =
1940        sha256::Hash::from_str(preimage).map_err(|_| LnurlError::general("Invalid preimage"))?;
1941    let preimage = preimage.as_byte_array();
1942    let result: AesSuccessActionDataResult = match (data, preimage).try_into() {
1943        Ok(data) => AesSuccessActionDataResult::Decrypted { data },
1944        Err(e) => AesSuccessActionDataResult::ErrorStatus {
1945            reason: e.to_string(),
1946        },
1947    };
1948
1949    Ok(Some(SuccessActionProcessed::Aes { result }))
1950}
1951
1952async fn emit_payment_status(event_emitter: &EventEmitter, payment: Payment) {
1953    match payment.status {
1954        PaymentStatus::Completed => {
1955            event_emitter
1956                .emit(&SdkEvent::PaymentSucceeded { payment })
1957                .await;
1958        }
1959        PaymentStatus::Failed => {
1960            event_emitter
1961                .emit(&SdkEvent::PaymentFailed { payment })
1962                .await;
1963        }
1964        PaymentStatus::Pending => {
1965            event_emitter
1966                .emit(&SdkEvent::PaymentPending { payment })
1967                .await;
1968        }
1969    }
1970}
1971
1972fn validate_breez_api_key(api_key: &str) -> Result<(), SdkError> {
1973    let api_key_decoded = base64::engine::general_purpose::STANDARD
1974        .decode(api_key.as_bytes())
1975        .map_err(|err| {
1976            SdkError::Generic(format!(
1977                "Could not base64 decode the Breez API key: {err:?}"
1978            ))
1979        })?;
1980    let (_rem, cert) = parse_x509_certificate(&api_key_decoded).map_err(|err| {
1981        SdkError::Generic(format!("Invalid certificate for Breez API key: {err:?}"))
1982    })?;
1983
1984    let issuer = cert
1985        .issuer()
1986        .iter_common_name()
1987        .next()
1988        .and_then(|cn| cn.as_str().ok());
1989    match issuer {
1990        Some(common_name) => {
1991            if !common_name.starts_with("Breez") {
1992                return Err(SdkError::Generic(
1993                    "Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted"
1994                        .to_string()
1995                ));
1996            }
1997        }
1998        _ => {
1999            return Err(SdkError::Generic(
2000                "Could not parse Breez API key certificate: issuer is invalid or not found."
2001                    .to_string(),
2002            ));
2003        }
2004    }
2005
2006    Ok(())
2007}