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 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/// `BreezSDK` is a wrapper around `SparkSDK` that provides a more structured API
156/// with request/response objects and comprehensive error handling.
157#[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/// Connects to the Spark network using the provided configuration and mnemonic.
187///
188/// # Arguments
189///
190/// * `request` - The connection request object
191///
192/// # Returns
193///
194/// Result containing either the initialized `BreezSdk` or an `SdkError`
195#[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, // every 1 minute
214        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    /// Creates a new instance of the `BreezSdk`
239    pub(crate) fn init_and_start(params: BreezSdkParams) -> Result<Self, SdkError> {
240        // In Regtest we allow running without a Breez API key to facilitate local
241        // integration tests. For non-regtest networks, a valid API key is required.
242        if !matches!(params.config.network, Network::Regtest) {
243            match &params.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    /// Starts the SDK's background tasks
273    ///
274    /// This method initiates the following backround tasks:
275    /// 1. `spawn_spark_private_mode_initialization`: initializes the spark private mode on startup
276    /// 2. `periodic_sync`: syncs the wallet with the Spark network    
277    /// 3. `try_recover_lightning_address`: recovers the lightning address on startup
278    /// 4. `spawn_zap_receipt_publisher`: publishes zap receipts for payments with zap requests
279    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    /// Refreshes the user's lightning address on the server on startup.
296    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    /// Background task that publishes zap receipts for payments with zap requests.
315    /// Triggered on startup and after syncing lnurl metadata.
316    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                // Create the zap receipt using NostrClient
389                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                // Publish the zap receipt via the server
402                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                    // Ensure we sync at least the configured interval
507                    () = 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                    // Insert the payment into storage to make it immediately available for listing
540                    if let Err(e) = self.storage.insert_payment(payment.clone()).await {
541                        error!("Failed to insert succeeded payment: {e:?}");
542                    }
543
544                    // Ensure potential lnurl metadata is synced before emitting the event.
545                    // Note this is already synced at TransferClaimStarting, but it might not have completed yet, so that could race.
546                    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                    // Insert the payment into storage to make it immediately available for listing
563                    if let Err(e) = self.storage.insert_payment(payment.clone()).await {
564                        error!("Failed to insert pending payment: {e:?}");
565                    }
566
567                    // Ensure potential lnurl metadata is synced before emitting the event
568                    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            // Already have lnurl metadata
600            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 there is a description hash, we assume this is a lnurl payment.
620        if details.description_hash.is_none() {
621            return;
622        }
623
624        // Let's check whether the lnurl receive metadata was already synced, then return early
625        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        // Just sync all lnurl metadata here, no need to be picky.
636        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    /// Synchronizes wallet state to persistent storage, making sure we have the latest balances and payments.
797    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                // No more invoices to fetch
907                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                // Check if already initialized in storage
975                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                    // Initialize if not already done
982                    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        // Enable spark private mode
1000        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    /// Registers a listener to receive SDK events
1016    ///
1017    /// # Arguments
1018    ///
1019    /// * `listener` - An implementation of the `EventListener` trait
1020    ///
1021    /// # Returns
1022    ///
1023    /// A unique identifier for the listener, which can be used to remove it later
1024    pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
1025        self.event_emitter.add_listener(listener).await
1026    }
1027
1028    /// Removes a previously registered event listener
1029    ///
1030    /// # Arguments
1031    ///
1032    /// * `id` - The listener ID returned from `add_event_listener`
1033    ///
1034    /// # Returns
1035    ///
1036    /// `true` if the listener was found and removed, `false` otherwise
1037    pub async fn remove_event_listener(&self, id: &str) -> bool {
1038        self.event_emitter.remove_listener(id).await
1039    }
1040
1041    /// Stops the SDK's background tasks
1042    ///
1043    /// This method stops the background tasks started by the `start()` method.
1044    /// It should be called before your application terminates to ensure proper cleanup.
1045    ///
1046    /// # Returns
1047    ///
1048    /// Result containing either success or an `SdkError` if the background task couldn't be stopped
1049    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    /// Returns the balance of the wallet in satoshis
1065    #[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                // TODO: allow passing amount
1130
1131                let object_repository = ObjectCacheRepository::new(self.storage.clone());
1132
1133                // First lookup in storage cache
1134                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                // Then query existing addresses
1144                let deposit_addresses = self
1145                    .spark_wallet
1146                    .list_static_deposit_addresses(None)
1147                    .await?;
1148
1149                // In case there are no addresses, generate a new one and cache it
1150                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        // Check if there is a claimable HTLC with the given payment hash
1198        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        // Insert the payment into storage to make it immediately available for listing
1216        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    /// Performs an LNURL withdraw operation for the amount of satoshis to
1343    /// withdraw and the LNURL withdraw request details. The LNURL withdraw request
1344    /// details can be obtained from calling [`BreezSdk::parse`].
1345    ///
1346    /// The method generates a Lightning invoice for the withdraw amount, stores
1347    /// the LNURL withdraw metadata, and performs the LNURL withdraw using  the generated
1348    /// invoice.
1349    ///
1350    /// If the `completion_timeout_secs` parameter is provided and greater than 0, the
1351    /// method will wait for the payment to be completed within that period. If the
1352    /// withdraw is completed within the timeout, the `payment` field in the response
1353    /// will be set with the payment details. If the `completion_timeout_secs`
1354    /// parameter is not provided or set to 0, the method will not wait for the payment
1355    /// to be completed. If the withdraw is not completed within the
1356    /// timeout, the `payment` field will be empty.
1357    ///
1358    /// # Arguments
1359    ///
1360    /// * `request` - The LNURL withdraw request
1361    ///
1362    /// # Returns
1363    ///
1364    /// Result containing either:
1365    /// * `LnurlWithdrawResponse` - The payment details if the withdraw request was successful
1366    /// * `SdkError` - If there was an error during the withdraw process
1367    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        // Generate a Lightning invoice for the withdraw
1386        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        // Store the LNURL withdraw metadata before executing the withdraw
1397        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        // Perform the LNURL withdraw using the generated invoice
1406        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        // Wait for the payment to be completed
1429        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    /// Synchronizes the wallet with the Spark network
1555    #[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    /// Lists payments from the storage with pagination
1573    ///
1574    /// This method provides direct access to the payment history stored in the database.
1575    /// It returns payments in reverse chronological order (newest first).
1576    ///
1577    /// # Arguments
1578    ///
1579    /// * `request` - Contains pagination parameters (offset and limit)
1580    ///
1581    /// # Returns
1582    ///
1583    /// * `Ok(ListPaymentsResponse)` - Contains the list of payments if successful
1584    /// * `Err(SdkError)` - If there was an error accessing the storage
1585    ///
1586    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        // Store the refund transaction details separately
1668        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        // Ensure spark private mode is initialized before registering
1719        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(&params).await?;
1741        cache.delete_lightning_address().await?;
1742        Ok(())
1743    }
1744
1745    /// List fiat currencies for which there is a known exchange rate,
1746    /// sorted by the canonical name of the currency.
1747    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    /// List the latest rates of fiat currencies, sorted by name.
1759    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    /// Get the recommended BTC fees based on the configured chain service.
1771    pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
1772        Ok(self.chain_service.recommended_fees().await?)
1773    }
1774
1775    /// Returns the metadata for the given token identifiers.
1776    ///
1777    /// Results are not guaranteed to be in the same order as the input token identifiers.    
1778    ///
1779    /// If the metadata is not found locally in cache, it will be queried from
1780    /// the Spark network and then cached.
1781    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    /// Signs a message with the wallet's identity key. The message is SHA256
1801    /// hashed before signing. The returned signature will be hex encoded in
1802    /// DER format by default, or compact format if specified.
1803    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    /// Verifies a message signature against the provided public key. The message
1822    /// is SHA256 hashed before verification. The signature can be hex encoded
1823    /// in either DER or compact format.
1824    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    /// Returns the user settings for the wallet.
1847    ///
1848    /// Some settings are fetched from the Spark network so network requests are performed.
1849    pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
1850        // Ensure spark private mode is initialized to avoid race conditions with the initialization task.
1851        self.ensure_spark_private_mode_initialized().await?;
1852
1853        let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
1854
1855        // We may in the future have user settings that are stored locally and synced using real-time sync.
1856
1857        Ok(UserSettings {
1858            spark_private_mode_enabled: spark_user_settings.private_enabled,
1859        })
1860    }
1861
1862    /// Updates the user settings for the wallet.
1863    ///
1864    /// Some settings are updated on the Spark network so network requests may be performed.
1865    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            // Reregister the lightning address if spark private mode changed.
1875            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    /// Returns an instance of the [`TokenIssuer`] for managing token issuance.
1899    pub fn get_token_issuer(&self) -> TokenIssuer {
1900        TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
1901    }
1902}
1903
1904// Separate impl block to avoid exposing private methods to uniffi.
1905impl 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 an idempotency key is provided, check if a payment with that id already exists
1919            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 HTLC options are provided, send an HTLC transfer
1995        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        // Insert the payment into storage to make it immediately available for listing
2030        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        // Insert the payment into storage to make it immediately available for listing
2070        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        // Insert the payment into storage to make it immediately available for listing
2128        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            // We are not sending amount in case the invoice contains it.
2142            Some(_) => None,
2143            // We are sending amount for zero amount invoice
2144            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        // Insert the payment into storage to make it immediately available for listing
2206        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        // First check if we already have the completed payment in storage
2260        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    // Pools the lightning send payment untill it is in completed state.
2302    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    /// Attempts to recover a lightning address from the lnurl server.
2351    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        // Query settings directly from spark wallet to avoid recursion through get_user_settings()
2392        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(&params).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}