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 flashnet::{
23    ClawbackRequest, ClawbackResponse, ExecuteSwapRequest, FlashnetClient, FlashnetError,
24    GetMinAmountsRequest, ListPoolsRequest, PoolSortOrder, SimulateSwapRequest,
25};
26use lnurl_models::sanitize_username;
27use spark_wallet::{
28    ExitSpeed, InvoiceDescription, ListTokenTransactionsRequest, ListTransfersRequest, Preimage,
29    SparkAddress, SparkWallet, TransferId, TransferTokenOutput, WalletEvent, WalletTransfer,
30};
31use std::{collections::HashMap, str::FromStr, sync::Arc};
32use tracing::{debug, error, info, trace, warn};
33use web_time::{Duration, SystemTime};
34
35use tokio::{
36    select,
37    sync::{Mutex, OnceCell, mpsc, oneshot, watch},
38    time::timeout,
39};
40use tokio_with_wasm::alias as tokio;
41use web_time::Instant;
42use x509_parser::parse_x509_certificate;
43
44use crate::{
45    AssetFilter, BitcoinAddressDetails, BitcoinChainService, Bolt11InvoiceDetails,
46    CheckLightningAddressRequest, CheckMessageRequest, CheckMessageResponse, ClaimDepositRequest,
47    ClaimDepositResponse, ClaimHtlcPaymentRequest, ClaimHtlcPaymentResponse, ConversionEstimate,
48    ConversionInfo, ConversionOptions, ConversionPurpose, ConversionStatus, ConversionType,
49    DepositInfo, ExternalInputParser, FetchConversionLimitsRequest, FetchConversionLimitsResponse,
50    GetPaymentRequest, GetPaymentResponse, GetTokensMetadataRequest, GetTokensMetadataResponse,
51    InputType, LightningAddressInfo, ListFiatCurrenciesResponse, ListFiatRatesResponse,
52    ListUnclaimedDepositsRequest, ListUnclaimedDepositsResponse, LnurlAuthRequestDetails,
53    LnurlCallbackStatus, LnurlPayInfo, LnurlPayRequest, LnurlPayResponse, LnurlWithdrawInfo,
54    LnurlWithdrawRequest, LnurlWithdrawResponse, Logger, MaxFee, Network, OnchainConfirmationSpeed,
55    OptimizationConfig, OptimizationProgress, PaymentDetails, PaymentDetailsFilter, PaymentStatus,
56    PaymentType, PrepareLnurlPayRequest, PrepareLnurlPayResponse, RefundDepositRequest,
57    RefundDepositResponse, RegisterLightningAddressRequest, SendOnchainFeeQuote,
58    SendPaymentOptions, SetLnurlMetadataItem, SignMessageRequest, SignMessageResponse,
59    SparkHtlcOptions, SparkInvoiceDetails, TokenConversionPool, TokenConversionResponse,
60    UpdateUserSettingsRequest, UserSettings, WaitForPaymentIdentifier,
61    chain::RecommendedFees,
62    error::SdkError,
63    events::{EventEmitter, EventListener, InternalSyncedEvent, SdkEvent},
64    issuer::TokenIssuer,
65    lnurl::{ListMetadataRequest, LnurlServerClient, PublishZapReceiptRequest},
66    logger,
67    models::{
68        Config, GetInfoRequest, GetInfoResponse, ListPaymentsRequest, ListPaymentsResponse,
69        Payment, PrepareSendPaymentRequest, PrepareSendPaymentResponse, ReceivePaymentMethod,
70        ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentMethod, SendPaymentRequest,
71        SendPaymentResponse, SyncWalletRequest, SyncWalletResponse,
72    },
73    nostr::NostrClient,
74    persist::{
75        CachedAccountInfo, ObjectCacheRepository, PaymentMetadata, StaticDepositAddress, Storage,
76        UpdateDepositPayload,
77    },
78    sync::SparkSyncService,
79    utils::{
80        deposit_chain_syncer::DepositChainSyncer,
81        run_with_shutdown,
82        send_payment_validation::validate_prepare_send_payment_request,
83        token::{
84            get_tokens_metadata_cached_or_query, map_and_persist_token_transaction,
85            token_transaction_to_payments,
86        },
87        utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo},
88    },
89};
90
91pub async fn parse_input(
92    input: &str,
93    external_input_parsers: Option<Vec<ExternalInputParser>>,
94) -> Result<InputType, SdkError> {
95    Ok(breez_sdk_common::input::parse(
96        input,
97        external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
98    )
99    .await?
100    .into())
101}
102
103#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
104const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
105
106#[cfg(all(target_family = "wasm", target_os = "unknown"))]
107const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
108
109const CLAIM_TX_SIZE_VBYTES: u64 = 99;
110const SYNC_PAGING_LIMIT: u32 = 100;
111/// Default maximum slippage for conversions in basis points (0.5%)
112const DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS: u32 = 50;
113/// Default timeout for conversion operations in seconds
114const DEFAULT_TOKEN_CONVERSION_TIMEOUT_SECS: u32 = 30;
115
116bitflags! {
117    #[derive(Clone, Debug)]
118    struct SyncType: u32 {
119        const Wallet = 1 << 0;
120        const WalletState = 1 << 1;
121        const Deposits = 1 << 2;
122        const LnurlMetadata = 1 << 3;
123        const Full = Self::Wallet.0.0
124            | Self::WalletState.0.0
125            | Self::Deposits.0.0
126            | Self::LnurlMetadata.0.0;
127    }
128}
129
130#[derive(Clone, Debug)]
131struct SyncRequest {
132    sync_type: SyncType,
133    #[allow(clippy::type_complexity)]
134    reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
135}
136
137impl SyncRequest {
138    fn new(reply: oneshot::Sender<Result<(), SdkError>>, sync_type: SyncType) -> Self {
139        Self {
140            sync_type,
141            reply: Arc::new(Mutex::new(Some(reply))),
142        }
143    }
144
145    fn full(reply: Option<oneshot::Sender<Result<(), SdkError>>>) -> Self {
146        Self {
147            sync_type: SyncType::Full,
148            reply: Arc::new(Mutex::new(reply)),
149        }
150    }
151
152    fn no_reply(sync_type: SyncType) -> Self {
153        Self {
154            sync_type,
155            reply: Arc::new(Mutex::new(None)),
156        }
157    }
158
159    async fn reply(&self, error: Option<SdkError>) {
160        if let Some(reply) = self.reply.lock().await.take() {
161            let _ = match error {
162                Some(e) => reply.send(Err(e)),
163                None => reply.send(Ok(())),
164            };
165        }
166    }
167}
168
169/// `BreezSDK` is a wrapper around `SparkSDK` that provides a more structured API
170/// with request/response objects and comprehensive error handling.
171#[derive(Clone)]
172#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
173pub struct BreezSdk {
174    config: Config,
175    spark_wallet: Arc<SparkWallet>,
176    storage: Arc<dyn Storage>,
177    chain_service: Arc<dyn BitcoinChainService>,
178    fiat_service: Arc<dyn FiatService>,
179    lnurl_client: Arc<dyn RestClient>,
180    lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
181    lnurl_auth_signer: Arc<crate::signer::lnurl_auth::LnurlAuthSignerAdapter>,
182    event_emitter: Arc<EventEmitter>,
183    shutdown_sender: watch::Sender<()>,
184    sync_trigger: tokio::sync::broadcast::Sender<SyncRequest>,
185    zap_receipt_trigger: tokio::sync::broadcast::Sender<()>,
186    conversion_refund_trigger: tokio::sync::broadcast::Sender<()>,
187    initial_synced_watcher: watch::Receiver<bool>,
188    external_input_parsers: Vec<ExternalInputParser>,
189    spark_private_mode_initialized: Arc<OnceCell<()>>,
190    nostr_client: Arc<NostrClient>,
191    flashnet_client: Arc<FlashnetClient>,
192}
193
194#[cfg_attr(feature = "uniffi", uniffi::export)]
195pub fn init_logging(
196    log_dir: Option<String>,
197    app_logger: Option<Box<dyn Logger>>,
198    log_filter: Option<String>,
199) -> Result<(), SdkError> {
200    logger::init_logging(log_dir, app_logger, log_filter)
201}
202
203/// Connects to the Spark network using the provided configuration and mnemonic.
204///
205/// # Arguments
206///
207/// * `request` - The connection request object
208///
209/// # Returns
210///
211/// Result containing either the initialized `BreezSdk` or an `SdkError`
212#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
213#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
214pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
215    let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
216        .with_default_storage(request.storage_dir);
217    let sdk = builder.build().await?;
218    Ok(sdk)
219}
220
221/// Connects to the Spark network using an external signer.
222///
223/// This method allows using a custom signer implementation instead of providing
224/// a seed directly.
225///
226/// # Arguments
227///
228/// * `request` - The connection request object with external signer
229///
230/// # Returns
231///
232/// Result containing either the initialized `BreezSdk` or an `SdkError`
233#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
234#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
235pub async fn connect_with_signer(
236    request: crate::ConnectWithSignerRequest,
237) -> Result<BreezSdk, SdkError> {
238    let builder = super::sdk_builder::SdkBuilder::new_with_signer(request.config, request.signer)
239        .with_default_storage(request.storage_dir);
240    let sdk = builder.build().await?;
241    Ok(sdk)
242}
243
244#[cfg_attr(feature = "uniffi", uniffi::export)]
245pub fn default_config(network: Network) -> Config {
246    let lnurl_domain = match network {
247        Network::Mainnet => Some("breez.tips".to_string()),
248        Network::Regtest => None,
249    };
250    Config {
251        api_key: None,
252        network,
253        sync_interval_secs: 60, // every 1 minute
254        max_deposit_claim_fee: Some(MaxFee::Rate { sat_per_vbyte: 1 }),
255        lnurl_domain,
256        prefer_spark_over_lightning: false,
257        external_input_parsers: None,
258        use_default_external_input_parsers: true,
259        real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
260        private_enabled_default: true,
261        optimization_config: OptimizationConfig {
262            auto_enabled: true,
263            multiplicity: 1,
264        },
265    }
266}
267
268/// Creates a default external signer from a mnemonic.
269///
270/// This is a convenience factory method for creating a signer that can be used
271/// with `connect_with_signer` or `SdkBuilder::new_with_signer`.
272///
273/// # Arguments
274///
275/// * `mnemonic` - BIP39 mnemonic phrase (12 or 24 words)
276/// * `passphrase` - Optional passphrase for the mnemonic
277/// * `network` - Network to use (Mainnet or Regtest)
278/// * `key_set_config` - Optional key set configuration. If None, uses default configuration.
279///
280/// # Returns
281///
282/// Result containing the signer as `Arc<dyn ExternalSigner>`
283#[cfg_attr(feature = "uniffi", uniffi::export)]
284pub fn default_external_signer(
285    mnemonic: String,
286    passphrase: Option<String>,
287    network: Network,
288    key_set_config: Option<crate::models::KeySetConfig>,
289) -> Result<Arc<dyn crate::signer::ExternalSigner>, SdkError> {
290    use crate::signer::DefaultExternalSigner;
291
292    let config = key_set_config.unwrap_or_default();
293    let signer = DefaultExternalSigner::new(
294        mnemonic,
295        passphrase,
296        network,
297        config.key_set_type,
298        config.use_address_index,
299        config.account_number,
300    )?;
301
302    Ok(Arc::new(signer))
303}
304
305pub(crate) struct BreezSdkParams {
306    pub config: Config,
307    pub storage: Arc<dyn Storage>,
308    pub chain_service: Arc<dyn BitcoinChainService>,
309    pub fiat_service: Arc<dyn FiatService>,
310    pub lnurl_client: Arc<dyn RestClient>,
311    pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
312    pub lnurl_auth_signer: Arc<crate::signer::lnurl_auth::LnurlAuthSignerAdapter>,
313    pub shutdown_sender: watch::Sender<()>,
314    pub spark_wallet: Arc<SparkWallet>,
315    pub event_emitter: Arc<EventEmitter>,
316    pub nostr_client: Arc<NostrClient>,
317    pub flashnet_client: Arc<FlashnetClient>,
318}
319
320impl BreezSdk {
321    /// Creates a new instance of the `BreezSdk`
322    pub(crate) fn init_and_start(params: BreezSdkParams) -> Result<Self, SdkError> {
323        // In Regtest we allow running without a Breez API key to facilitate local
324        // integration tests. For non-regtest networks, a valid API key is required.
325        if !matches!(params.config.network, Network::Regtest) {
326            match &params.config.api_key {
327                Some(api_key) => validate_breez_api_key(api_key)?,
328                None => return Err(SdkError::Generic("Missing Breez API key".to_string())),
329            }
330        }
331        let (initial_synced_sender, initial_synced_watcher) = watch::channel(false);
332        let external_input_parsers = params.config.get_all_external_input_parsers();
333        let sdk = Self {
334            config: params.config,
335            spark_wallet: params.spark_wallet,
336            storage: params.storage,
337            chain_service: params.chain_service,
338            fiat_service: params.fiat_service,
339            lnurl_client: params.lnurl_client,
340            lnurl_server_client: params.lnurl_server_client,
341            lnurl_auth_signer: params.lnurl_auth_signer,
342            event_emitter: params.event_emitter,
343            shutdown_sender: params.shutdown_sender,
344            sync_trigger: tokio::sync::broadcast::channel(10).0,
345            zap_receipt_trigger: tokio::sync::broadcast::channel(10).0,
346            conversion_refund_trigger: tokio::sync::broadcast::channel(10).0,
347            initial_synced_watcher,
348            external_input_parsers,
349            spark_private_mode_initialized: Arc::new(OnceCell::new()),
350            nostr_client: params.nostr_client,
351            flashnet_client: params.flashnet_client,
352        };
353
354        sdk.start(initial_synced_sender);
355        Ok(sdk)
356    }
357
358    /// Starts the SDK's background tasks
359    ///
360    /// This method initiates the following backround tasks:
361    /// 1. `spawn_spark_private_mode_initialization`: initializes the spark private mode on startup
362    /// 2. `periodic_sync`: syncs the wallet with the Spark network    
363    /// 3. `try_recover_lightning_address`: recovers the lightning address on startup
364    /// 4. `spawn_zap_receipt_publisher`: publishes zap receipts for payments with zap requests
365    /// 5. `spawm_conversion_refunder`: refunds failed conversions
366    fn start(&self, initial_synced_sender: watch::Sender<bool>) {
367        self.spawn_spark_private_mode_initialization();
368        self.periodic_sync(initial_synced_sender);
369        self.try_recover_lightning_address();
370        self.spawn_zap_receipt_publisher();
371        self.spawn_conversion_refunder();
372    }
373
374    fn spawn_spark_private_mode_initialization(&self) {
375        let sdk = self.clone();
376        tokio::spawn(async move {
377            if let Err(e) = sdk.ensure_spark_private_mode_initialized().await {
378                error!("Failed to initialize spark private mode: {e:?}");
379            }
380        });
381    }
382
383    /// Refreshes the user's lightning address on the server on startup.
384    fn try_recover_lightning_address(&self) {
385        let sdk = self.clone();
386        tokio::spawn(async move {
387            if sdk.config.lnurl_domain.is_none() {
388                return;
389            }
390
391            match sdk.recover_lightning_address().await {
392                Ok(None) => info!("no lightning address to recover on startup"),
393                Ok(Some(value)) => info!(
394                    "recovered lightning address on startup: lnurl: {}, address: {}",
395                    value.lnurl, value.lightning_address
396                ),
397                Err(e) => error!("Failed to recover lightning address on startup: {e:?}"),
398            }
399        });
400    }
401
402    /// Background task that publishes zap receipts for payments with zap requests.
403    /// Triggered on startup and after syncing lnurl metadata.
404    fn spawn_zap_receipt_publisher(&self) {
405        let sdk = self.clone();
406        let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
407        let mut trigger_receiver = sdk.zap_receipt_trigger.clone().subscribe();
408
409        tokio::spawn(async move {
410            if let Err(e) = Self::process_pending_zap_receipts(&sdk).await {
411                error!("Failed to process pending zap receipts on startup: {e:?}");
412            }
413
414            loop {
415                tokio::select! {
416                    _ = shutdown_receiver.changed() => {
417                        info!("Zap receipt publisher shutdown signal received");
418                        return;
419                    }
420                    _ = trigger_receiver.recv() => {
421                        if let Err(e) = Self::process_pending_zap_receipts(&sdk).await {
422                            error!("Failed to process pending zap receipts: {e:?}");
423                        }
424                    }
425                }
426            }
427        });
428    }
429
430    /// Background task that periodically checks for failed conversions and refunds them.
431    /// Triggered on startup and then every 150 seconds.
432    fn spawn_conversion_refunder(&self) {
433        let sdk = self.clone();
434        let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
435        let mut trigger_receiver = sdk.conversion_refund_trigger.clone().subscribe();
436
437        tokio::spawn(async move {
438            loop {
439                if let Err(e) = sdk.refund_failed_conversions().await {
440                    error!("Failed to refund failed conversions: {e:?}");
441                }
442
443                select! {
444                    _ = shutdown_receiver.changed() => {
445                        info!("Conversion refunder shutdown signal received");
446                        return;
447                    }
448                    _ = trigger_receiver.recv() => {
449                        debug!("Conversion refunder triggered");
450                    }
451                    () = tokio::time::sleep(Duration::from_secs(150)) => {}
452                }
453            }
454        });
455    }
456
457    async fn process_pending_zap_receipts(&self) -> Result<(), SdkError> {
458        let Some(lnurl_server_client) = self.lnurl_server_client.clone() else {
459            return Ok(());
460        };
461
462        let mut offset = 0;
463        let limit = 100;
464        loop {
465            let payments = self
466                .storage
467                .list_payments(ListPaymentsRequest {
468                    offset: Some(offset),
469                    limit: Some(limit),
470                    status_filter: Some(vec![PaymentStatus::Completed]),
471                    type_filter: Some(vec![PaymentType::Receive]),
472                    asset_filter: Some(AssetFilter::Bitcoin),
473                    ..Default::default()
474                })
475                .await?;
476            if payments.is_empty() {
477                break;
478            }
479
480            let len = u32::try_from(payments.len())?;
481            for payment in payments {
482                let Some(PaymentDetails::Lightning {
483                    ref lnurl_receive_metadata,
484                    ref payment_hash,
485                    ..
486                }) = payment.details
487                else {
488                    continue;
489                };
490
491                let Some(lnurl_receive_metadata) = lnurl_receive_metadata else {
492                    continue;
493                };
494
495                let Some(zap_request) = &lnurl_receive_metadata.nostr_zap_request else {
496                    continue;
497                };
498
499                if lnurl_receive_metadata.nostr_zap_receipt.is_some() {
500                    continue;
501                }
502
503                // Create the zap receipt using NostrClient
504                let zap_receipt = match self
505                    .nostr_client
506                    .create_zap_receipt(zap_request, &payment)
507                    .await
508                {
509                    Ok(receipt) => receipt,
510                    Err(e) => {
511                        error!(
512                            "Failed to create zap receipt for payment {}: {e:?}",
513                            payment.id
514                        );
515                        continue;
516                    }
517                };
518
519                // Publish the zap receipt via the server
520                let zap_receipt = match lnurl_server_client
521                    .publish_zap_receipt(&PublishZapReceiptRequest {
522                        payment_hash: payment_hash.clone(),
523                        zap_receipt: zap_receipt.clone(),
524                    })
525                    .await
526                {
527                    Ok(zap_receipt) => zap_receipt,
528                    Err(e) => {
529                        error!(
530                            "Failed to publish zap receipt for payment {}: {}",
531                            payment.id, e
532                        );
533                        continue;
534                    }
535                };
536
537                if let Err(e) = self
538                    .storage
539                    .set_lnurl_metadata(vec![SetLnurlMetadataItem {
540                        sender_comment: lnurl_receive_metadata.sender_comment.clone(),
541                        nostr_zap_request: Some(zap_request.clone()),
542                        nostr_zap_receipt: Some(zap_receipt),
543                        payment_hash: payment_hash.clone(),
544                    }])
545                    .await
546                {
547                    error!(
548                        "Failed to store zap receipt for payment {}: {}",
549                        payment.id, e
550                    );
551                }
552            }
553
554            if len < limit {
555                break;
556            }
557
558            offset = offset.saturating_add(len);
559        }
560
561        Ok(())
562    }
563
564    fn periodic_sync(&self, initial_synced_sender: watch::Sender<bool>) {
565        let sdk = self.clone();
566        let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
567        let mut subscription = sdk.spark_wallet.subscribe_events();
568        let sync_trigger_sender = sdk.sync_trigger.clone();
569        let mut sync_trigger_receiver = sdk.sync_trigger.clone().subscribe();
570        let mut last_sync_time = SystemTime::now();
571        let sync_interval = u64::from(self.config.sync_interval_secs);
572        tokio::spawn(async move {
573            let balance_watcher =
574                BalanceWatcher::new(sdk.spark_wallet.clone(), sdk.storage.clone());
575            let balance_watcher_id = sdk.add_event_listener(Box::new(balance_watcher)).await;
576            loop {
577                tokio::select! {
578                    _ = shutdown_receiver.changed() => {
579                        if !sdk.remove_event_listener(&balance_watcher_id).await {
580                            error!("Failed to remove balance watcher listener");
581                        }
582                        info!("Deposit tracking loop shutdown signal received");
583                        return;
584                    }
585                    event = subscription.recv() => {
586                        match event {
587                            Ok(event) => {
588                                info!("Received event: {event}");
589                                trace!("Received event: {:?}", event);
590                                sdk.handle_wallet_event(event).await;
591                            }
592                            Err(e) => {
593                                error!("Failed to receive event: {e:?}");
594                            }
595                        }
596                    }
597                    sync_type_res = sync_trigger_receiver.recv() => {
598                        let Ok(sync_request) = sync_type_res else {
599                            continue;
600                        };
601                        info!("Sync trigger changed: {:?}", &sync_request);
602                        let cloned_sdk = sdk.clone();
603                        let initial_synced_sender = initial_synced_sender.clone();
604                        if let Some(true) = Box::pin(run_with_shutdown(shutdown_receiver.clone(), "Sync trigger changed", async move {
605                            if let Err(e) = cloned_sdk.sync_wallet_internal(sync_request.sync_type.clone()).await {
606                                error!("Failed to sync wallet: {e:?}");
607                                let () = sync_request.reply(Some(e)).await;
608                                return false;
609                            }
610                            // Notify that the requested sync is complete
611                            let () = sync_request.reply(None).await;
612                            // If this was a full sync, notify the initial synced watcher
613                            if sync_request.sync_type.contains(SyncType::Full) {
614                                if let Err(e) = initial_synced_sender.send(true) {
615                                    error!("Failed to send initial synced signal: {e:?}");
616                                }
617                                return true;
618                            }
619
620                            false
621                        })).await {
622                            last_sync_time = SystemTime::now();
623                        }
624                    }
625                    // Ensure we sync at least the configured interval
626                    () = tokio::time::sleep(Duration::from_secs(10)) => {
627                        let now = SystemTime::now();
628                        if let Ok(elapsed) = now.duration_since(last_sync_time) && elapsed.as_secs() >= sync_interval
629                            && let Err(e) = sync_trigger_sender.send(SyncRequest::full(None)) {
630                            error!("Failed to trigger periodic sync: {e:?}");
631                        }
632                    }
633                }
634            }
635        });
636    }
637
638    async fn handle_wallet_event(&self, event: WalletEvent) {
639        match event {
640            WalletEvent::DepositConfirmed(_) => {
641                info!("Deposit confirmed");
642            }
643            WalletEvent::StreamConnected => {
644                info!("Stream connected");
645            }
646            WalletEvent::StreamDisconnected => {
647                info!("Stream disconnected");
648            }
649            WalletEvent::Synced => {
650                info!("Synced");
651                if let Err(e) = self.sync_trigger.send(SyncRequest::full(None)) {
652                    error!("Failed to sync wallet: {e:?}");
653                }
654            }
655            WalletEvent::TransferClaimed(transfer) => {
656                info!("Transfer claimed");
657                if let Ok(mut payment) = Payment::try_from(transfer) {
658                    // Insert the payment into storage to make it immediately available for listing
659                    if let Err(e) = self.storage.insert_payment(payment.clone()).await {
660                        error!("Failed to insert succeeded payment: {e:?}");
661                    }
662
663                    // Ensure potential lnurl metadata is synced before emitting the event.
664                    // Note this is already synced at TransferClaimStarting, but it might not have completed yet, so that could race.
665                    self.sync_single_lnurl_metadata(&mut payment).await;
666
667                    self.event_emitter
668                        .emit(&SdkEvent::PaymentSucceeded { payment })
669                        .await;
670                }
671                if let Err(e) = self
672                    .sync_trigger
673                    .send(SyncRequest::no_reply(SyncType::WalletState))
674                {
675                    error!("Failed to sync wallet: {e:?}");
676                }
677            }
678            WalletEvent::TransferClaimStarting(transfer) => {
679                info!("Transfer claim starting");
680                if let Ok(mut payment) = Payment::try_from(transfer) {
681                    // Insert the payment into storage to make it immediately available for listing
682                    if let Err(e) = self.storage.insert_payment(payment.clone()).await {
683                        error!("Failed to insert pending payment: {e:?}");
684                    }
685
686                    // Ensure potential lnurl metadata is synced before emitting the event
687                    self.sync_single_lnurl_metadata(&mut payment).await;
688
689                    self.event_emitter
690                        .emit(&SdkEvent::PaymentPending { payment })
691                        .await;
692                }
693                if let Err(e) = self
694                    .sync_trigger
695                    .send(SyncRequest::no_reply(SyncType::WalletState))
696                {
697                    error!("Failed to sync wallet: {e:?}");
698                }
699            }
700            WalletEvent::Optimization(event) => {
701                info!("Optimization event: {:?}", event);
702            }
703        }
704    }
705
706    async fn sync_single_lnurl_metadata(&self, payment: &mut Payment) {
707        if payment.payment_type != PaymentType::Receive {
708            return;
709        }
710
711        let Some(PaymentDetails::Lightning {
712            invoice,
713            lnurl_receive_metadata,
714            ..
715        }) = &mut payment.details
716        else {
717            return;
718        };
719
720        if lnurl_receive_metadata.is_some() {
721            // Already have lnurl metadata
722            return;
723        }
724
725        let Ok(input) = parse_input(invoice, None).await else {
726            error!(
727                "Failed to parse invoice for lnurl metadata sync: {}",
728                invoice
729            );
730            return;
731        };
732
733        let InputType::Bolt11Invoice(details) = input else {
734            error!(
735                "Input is not a Bolt11 invoice for lnurl metadata sync: {}",
736                invoice
737            );
738            return;
739        };
740
741        // If there is a description hash, we assume this is a lnurl payment.
742        if details.description_hash.is_none() {
743            return;
744        }
745
746        // Let's check whether the lnurl receive metadata was already synced, then return early
747        if let Ok(db_payment) = self.storage.get_payment_by_id(payment.id.clone()).await
748            && let Some(PaymentDetails::Lightning {
749                lnurl_receive_metadata: db_lnurl_receive_metadata,
750                ..
751            }) = db_payment.details
752        {
753            *lnurl_receive_metadata = db_lnurl_receive_metadata;
754            return;
755        }
756
757        // Just sync all lnurl metadata here, no need to be picky.
758        let (tx, rx) = oneshot::channel();
759        if let Err(e) = self
760            .sync_trigger
761            .send(SyncRequest::new(tx, SyncType::LnurlMetadata))
762        {
763            error!("Failed to trigger lnurl metadata sync: {e}");
764            return;
765        }
766
767        if let Err(e) = rx.await {
768            error!("Failed to sync lnurl metadata for invoice {}: {e}", invoice);
769            return;
770        }
771
772        let db_payment = match self.storage.get_payment_by_id(payment.id.clone()).await {
773            Ok(p) => p,
774            Err(e) => {
775                debug!("Payment not found in storage for invoice {}: {e}", invoice);
776                return;
777            }
778        };
779
780        let Some(PaymentDetails::Lightning {
781            lnurl_receive_metadata: db_lnurl_receive_metadata,
782            ..
783        }) = db_payment.details
784        else {
785            debug!(
786                "No lnurl receive metadata in storage for invoice {}",
787                invoice
788            );
789            return;
790        };
791        *lnurl_receive_metadata = db_lnurl_receive_metadata;
792    }
793
794    #[allow(clippy::too_many_lines)]
795    async fn sync_wallet_internal(&self, sync_type: SyncType) -> Result<(), SdkError> {
796        let start_time = Instant::now();
797
798        let sync_wallet = async {
799            let wallet_synced = if sync_type.contains(SyncType::Wallet) {
800                debug!("sync_wallet_internal: Starting Wallet sync");
801                let wallet_start = Instant::now();
802                match self.spark_wallet.sync().await {
803                    Ok(()) => {
804                        debug!(
805                            "sync_wallet_internal: Wallet sync completed in {:?}",
806                            wallet_start.elapsed()
807                        );
808                        true
809                    }
810                    Err(e) => {
811                        error!(
812                            "sync_wallet_internal: Spark wallet sync failed in {:?}: {e:?}",
813                            wallet_start.elapsed()
814                        );
815                        false
816                    }
817                }
818            } else {
819                trace!("sync_wallet_internal: Skipping Wallet sync");
820                false
821            };
822
823            let wallet_state_synced = if sync_type.contains(SyncType::WalletState) {
824                debug!("sync_wallet_internal: Starting WalletState sync");
825                let wallet_state_start = Instant::now();
826                match self.sync_wallet_state_to_storage().await {
827                    Ok(()) => {
828                        debug!(
829                            "sync_wallet_internal: WalletState sync completed in {:?}",
830                            wallet_state_start.elapsed()
831                        );
832                        true
833                    }
834                    Err(e) => {
835                        error!(
836                            "sync_wallet_internal: Failed to sync wallet state to storage in {:?}: {e:?}",
837                            wallet_state_start.elapsed()
838                        );
839                        false
840                    }
841                }
842            } else {
843                trace!("sync_wallet_internal: Skipping WalletState sync");
844                false
845            };
846
847            (wallet_synced, wallet_state_synced)
848        };
849
850        let sync_lnurl = async {
851            if sync_type.contains(SyncType::LnurlMetadata) {
852                debug!("sync_wallet_internal: Starting LnurlMetadata sync");
853                let lnurl_start = Instant::now();
854                match self.sync_lnurl_metadata().await {
855                    Ok(()) => {
856                        debug!(
857                            "sync_wallet_internal: LnurlMetadata sync completed in {:?}",
858                            lnurl_start.elapsed()
859                        );
860                        true
861                    }
862                    Err(e) => {
863                        error!(
864                            "sync_wallet_internal: Failed to sync lnurl metadata in {:?}: {e:?}",
865                            lnurl_start.elapsed()
866                        );
867                        false
868                    }
869                }
870            } else {
871                trace!("sync_wallet_internal: Skipping LnurlMetadata sync");
872                false
873            }
874        };
875
876        let sync_deposits = async {
877            if sync_type.contains(SyncType::Deposits) {
878                debug!("sync_wallet_internal: Starting Deposits sync");
879                let deposits_start = Instant::now();
880                match self.check_and_claim_static_deposits().await {
881                    Ok(()) => {
882                        debug!(
883                            "sync_wallet_internal: Deposits sync completed in {:?}",
884                            deposits_start.elapsed()
885                        );
886                        true
887                    }
888                    Err(e) => {
889                        error!(
890                            "sync_wallet_internal: Failed to check and claim static deposits in {:?}: {e:?}",
891                            deposits_start.elapsed()
892                        );
893                        false
894                    }
895                }
896            } else {
897                trace!("sync_wallet_internal: Skipping Deposits sync");
898                false
899            }
900        };
901
902        let ((wallet, wallet_state), lnurl_metadata, deposits) =
903            tokio::join!(sync_wallet, sync_lnurl, sync_deposits);
904
905        let elapsed = start_time.elapsed();
906        let event = InternalSyncedEvent {
907            wallet,
908            wallet_state,
909            lnurl_metadata,
910            deposits,
911            storage_incoming: None,
912        };
913        info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}: {event:?}");
914        self.event_emitter.emit_synced(&event).await;
915        Ok(())
916    }
917
918    /// Synchronizes wallet state to persistent storage, making sure we have the latest balances and payments.
919    async fn sync_wallet_state_to_storage(&self) -> Result<(), SdkError> {
920        update_balances(self.spark_wallet.clone(), self.storage.clone()).await?;
921
922        let initial_sync_complete = *self.initial_synced_watcher.borrow();
923        let sync_service = SparkSyncService::new(
924            self.spark_wallet.clone(),
925            self.storage.clone(),
926            self.event_emitter.clone(),
927        );
928        sync_service.sync_payments(initial_sync_complete).await?;
929
930        Ok(())
931    }
932
933    async fn check_and_claim_static_deposits(&self) -> Result<(), SdkError> {
934        self.ensure_spark_private_mode_initialized().await?;
935        let to_claim = DepositChainSyncer::new(
936            self.chain_service.clone(),
937            self.storage.clone(),
938            self.spark_wallet.clone(),
939        )
940        .sync()
941        .await?;
942
943        let mut claimed_deposits: Vec<DepositInfo> = Vec::new();
944        let mut unclaimed_deposits: Vec<DepositInfo> = Vec::new();
945        for detailed_utxo in to_claim {
946            match self
947                .claim_utxo(&detailed_utxo, self.config.max_deposit_claim_fee.clone())
948                .await
949            {
950                Ok(_) => {
951                    info!("Claimed utxo {}:{}", detailed_utxo.txid, detailed_utxo.vout);
952                    self.storage
953                        .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
954                        .await?;
955                    claimed_deposits.push(detailed_utxo.into());
956                }
957                Err(e) => {
958                    warn!(
959                        "Failed to claim utxo {}:{}: {e}",
960                        detailed_utxo.txid, detailed_utxo.vout
961                    );
962                    self.storage
963                        .update_deposit(
964                            detailed_utxo.txid.to_string(),
965                            detailed_utxo.vout,
966                            UpdateDepositPayload::ClaimError {
967                                error: e.clone().into(),
968                            },
969                        )
970                        .await?;
971                    let mut unclaimed_deposit: DepositInfo = detailed_utxo.clone().into();
972                    unclaimed_deposit.claim_error = Some(e.into());
973                    unclaimed_deposits.push(unclaimed_deposit);
974                }
975            }
976        }
977
978        info!("background claim completed, unclaimed deposits: {unclaimed_deposits:?}");
979
980        if !unclaimed_deposits.is_empty() {
981            self.event_emitter
982                .emit(&SdkEvent::UnclaimedDeposits { unclaimed_deposits })
983                .await;
984        }
985        if !claimed_deposits.is_empty() {
986            self.event_emitter
987                .emit(&SdkEvent::ClaimedDeposits { claimed_deposits })
988                .await;
989        }
990        Ok(())
991    }
992
993    async fn sync_lnurl_metadata(&self) -> Result<(), SdkError> {
994        let Some(lnurl_server_client) = self.lnurl_server_client.clone() else {
995            return Ok(());
996        };
997
998        let cache = ObjectCacheRepository::new(Arc::clone(&self.storage));
999        let mut updated_after = cache.fetch_lnurl_metadata_updated_after().await?;
1000
1001        loop {
1002            debug!("Syncing lnurl metadata from updated_after {updated_after}");
1003            let metadata = lnurl_server_client
1004                .list_metadata(&ListMetadataRequest {
1005                    offset: None,
1006                    limit: Some(SYNC_PAGING_LIMIT),
1007                    updated_after: Some(updated_after),
1008                })
1009                .await?;
1010
1011            if metadata.metadata.is_empty() {
1012                debug!("No more lnurl metadata on offset {updated_after}");
1013                break;
1014            }
1015
1016            let len = u32::try_from(metadata.metadata.len())?;
1017            let last_updated_at = metadata.metadata.last().map(|m| m.updated_at);
1018            self.storage
1019                .set_lnurl_metadata(metadata.metadata.into_iter().map(From::from).collect())
1020                .await?;
1021
1022            debug!(
1023                "Synchronized {} lnurl metadata at updated_after {updated_after}",
1024                len
1025            );
1026            updated_after = last_updated_at.unwrap_or(updated_after);
1027            cache
1028                .save_lnurl_metadata_updated_after(updated_after)
1029                .await?;
1030
1031            let _ = self.zap_receipt_trigger.send(());
1032            if len < SYNC_PAGING_LIMIT {
1033                // No more invoices to fetch
1034                break;
1035            }
1036        }
1037
1038        Ok(())
1039    }
1040
1041    /// Checks for payments that need conversion refunds and initiates the manual refund process.
1042    /// This occurs when a Spark transfer or token transaction is sent using the Flashnet client,
1043    /// but the execution fails and no automatic refund is initiated.
1044    async fn refund_failed_conversions(&self) -> Result<(), SdkError> {
1045        debug!("Checking for failed conversions needing refunds");
1046        let payments = self
1047            .storage
1048            .list_payments(ListPaymentsRequest {
1049                payment_details_filter: Some(vec![
1050                    PaymentDetailsFilter::Spark {
1051                        htlc_status: None,
1052                        conversion_refund_needed: Some(true),
1053                    },
1054                    PaymentDetailsFilter::Token {
1055                        conversion_refund_needed: Some(true),
1056                        tx_hash: None,
1057                    },
1058                ]),
1059                ..Default::default()
1060            })
1061            .await?;
1062        debug!(
1063            "Found {} payments needing conversion refunds",
1064            payments.len()
1065        );
1066        for payment in payments {
1067            if let Err(e) = self.refund_conversion(&payment).await {
1068                error!(
1069                    "Failed to refund conversion for payment {}: {e:?}",
1070                    payment.id
1071                );
1072            }
1073        }
1074
1075        Ok(())
1076    }
1077
1078    /// Initiates a refund for a conversion payment that requires a manual refund.
1079    async fn refund_conversion(&self, payment: &Payment) -> Result<(), SdkError> {
1080        let (clawback_id, conversion_info) = match &payment.details {
1081            Some(PaymentDetails::Spark {
1082                conversion_info, ..
1083            }) => (payment.id.clone(), conversion_info),
1084            Some(PaymentDetails::Token {
1085                tx_hash,
1086                conversion_info,
1087                ..
1088            }) => (tx_hash.clone(), conversion_info),
1089            _ => {
1090                return Err(SdkError::Generic(
1091                    "Payment is not a Spark or Conversion".to_string(),
1092                ));
1093            }
1094        };
1095        let Some(ConversionInfo {
1096            pool_id,
1097            conversion_id,
1098            status: ConversionStatus::RefundNeeded,
1099            fee,
1100            purpose,
1101        }) = conversion_info
1102        else {
1103            return Err(SdkError::Generic(
1104                "Conversion does not have a refund pending status".to_string(),
1105            ));
1106        };
1107        debug!(
1108            "Conversion refund needed for payment {}: pool_id {pool_id}, conversion_id {conversion_id}",
1109            payment.id
1110        );
1111        let Ok(pool_id) = PublicKey::from_str(pool_id) else {
1112            return Err(SdkError::Generic(format!("Invalid pool_id: {pool_id}")));
1113        };
1114        match self
1115            .flashnet_client
1116            .clawback(ClawbackRequest {
1117                pool_id,
1118                transfer_id: clawback_id,
1119            })
1120            .await
1121        {
1122            Ok(ClawbackResponse {
1123                accepted: true,
1124                spark_status_tracking_id,
1125                ..
1126            }) => {
1127                debug!(
1128                    "Clawback initiated for payment {}: tracking_id: {}",
1129                    payment.id, spark_status_tracking_id
1130                );
1131                // Update the payment metadata to reflect the refund status
1132                self.merge_payment_metadata(
1133                    payment.id.clone(),
1134                    PaymentMetadata {
1135                        conversion_info: Some(ConversionInfo {
1136                            pool_id: pool_id.to_string(),
1137                            conversion_id: conversion_id.clone(),
1138                            status: ConversionStatus::Refunded,
1139                            fee: *fee,
1140                            purpose: purpose.clone(),
1141                        }),
1142                        ..Default::default()
1143                    },
1144                )
1145                .await?;
1146                // Add payment metadata for the not yet received refund payment
1147                let cache = ObjectCacheRepository::new(self.storage.clone());
1148                cache
1149                    .save_payment_metadata(
1150                        &spark_status_tracking_id,
1151                        &PaymentMetadata {
1152                            conversion_info: Some(ConversionInfo {
1153                                pool_id: pool_id.to_string(),
1154                                conversion_id: conversion_id.clone(),
1155                                status: ConversionStatus::Refunded,
1156                                fee: Some(0),
1157                                purpose: None,
1158                            }),
1159                            ..Default::default()
1160                        },
1161                    )
1162                    .await?;
1163                Ok(())
1164            }
1165            Ok(ClawbackResponse {
1166                accepted: false,
1167                request_id,
1168                error,
1169                ..
1170            }) => Err(SdkError::Generic(format!(
1171                "Clawback not accepted: request_id: {request_id:?}, error: {error:?}"
1172            ))),
1173            Err(e) => Err(SdkError::Generic(format!(
1174                "Failed to initiate clawback: {e}"
1175            ))),
1176        }
1177    }
1178
1179    async fn claim_utxo(
1180        &self,
1181        detailed_utxo: &DetailedUtxo,
1182        max_claim_fee: Option<MaxFee>,
1183    ) -> Result<WalletTransfer, SdkError> {
1184        info!(
1185            "Fetching static deposit claim quote for deposit tx {}:{} and amount: {}",
1186            detailed_utxo.txid, detailed_utxo.vout, detailed_utxo.value
1187        );
1188        let quote = self
1189            .spark_wallet
1190            .fetch_static_deposit_claim_quote(detailed_utxo.tx.clone(), Some(detailed_utxo.vout))
1191            .await?;
1192
1193        let spark_requested_fee_sats = detailed_utxo.value.saturating_sub(quote.credit_amount_sats);
1194
1195        let spark_requested_fee_rate = spark_requested_fee_sats.div_ceil(CLAIM_TX_SIZE_VBYTES);
1196
1197        let Some(max_deposit_claim_fee) = max_claim_fee else {
1198            return Err(SdkError::MaxDepositClaimFeeExceeded {
1199                tx: detailed_utxo.txid.to_string(),
1200                vout: detailed_utxo.vout,
1201                max_fee: None,
1202                required_fee_sats: spark_requested_fee_sats,
1203                required_fee_rate_sat_per_vbyte: spark_requested_fee_rate,
1204            });
1205        };
1206        let max_fee = max_deposit_claim_fee
1207            .to_fee(self.chain_service.as_ref())
1208            .await?;
1209        let max_fee_sats = max_fee.to_sats(CLAIM_TX_SIZE_VBYTES);
1210        info!(
1211            "User max fee: {} spark requested fee: {}",
1212            max_fee_sats, spark_requested_fee_sats
1213        );
1214        if spark_requested_fee_sats > max_fee_sats {
1215            return Err(SdkError::MaxDepositClaimFeeExceeded {
1216                tx: detailed_utxo.txid.to_string(),
1217                vout: detailed_utxo.vout,
1218                max_fee: Some(max_fee),
1219                required_fee_sats: spark_requested_fee_sats,
1220                required_fee_rate_sat_per_vbyte: spark_requested_fee_rate,
1221            });
1222        }
1223
1224        info!(
1225            "Claiming static deposit for utxo {}:{}",
1226            detailed_utxo.txid, detailed_utxo.vout
1227        );
1228        let transfer = self.spark_wallet.claim_static_deposit(quote).await?;
1229        info!(
1230            "Claimed static deposit transfer for utxo {}:{}, value {}",
1231            detailed_utxo.txid, detailed_utxo.vout, transfer.total_value_sat,
1232        );
1233        Ok(transfer)
1234    }
1235
1236    async fn ensure_spark_private_mode_initialized(&self) -> Result<(), SdkError> {
1237        self.spark_private_mode_initialized
1238            .get_or_try_init(|| async {
1239                // Check if already initialized in storage
1240                let object_repository = ObjectCacheRepository::new(self.storage.clone());
1241                let is_initialized = object_repository
1242                    .fetch_spark_private_mode_initialized()
1243                    .await?;
1244
1245                if !is_initialized {
1246                    // Initialize if not already done
1247                    self.initialize_spark_private_mode().await?;
1248                }
1249                Ok::<_, SdkError>(())
1250            })
1251            .await?;
1252        Ok(())
1253    }
1254
1255    async fn initialize_spark_private_mode(&self) -> Result<(), SdkError> {
1256        if !self.config.private_enabled_default {
1257            ObjectCacheRepository::new(self.storage.clone())
1258                .save_spark_private_mode_initialized()
1259                .await?;
1260            info!("Spark private mode initialized: no changes needed");
1261            return Ok(());
1262        }
1263
1264        // Enable spark private mode
1265        self.update_user_settings(UpdateUserSettingsRequest {
1266            spark_private_mode_enabled: Some(true),
1267        })
1268        .await?;
1269        ObjectCacheRepository::new(self.storage.clone())
1270            .save_spark_private_mode_initialized()
1271            .await?;
1272        info!("Spark private mode initialized: enabled");
1273        Ok(())
1274    }
1275}
1276
1277#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
1278#[allow(clippy::needless_pass_by_value)]
1279impl BreezSdk {
1280    /// Registers a listener to receive SDK events
1281    ///
1282    /// # Arguments
1283    ///
1284    /// * `listener` - An implementation of the `EventListener` trait
1285    ///
1286    /// # Returns
1287    ///
1288    /// A unique identifier for the listener, which can be used to remove it later
1289    pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
1290        self.event_emitter.add_listener(listener).await
1291    }
1292
1293    /// Removes a previously registered event listener
1294    ///
1295    /// # Arguments
1296    ///
1297    /// * `id` - The listener ID returned from `add_event_listener`
1298    ///
1299    /// # Returns
1300    ///
1301    /// `true` if the listener was found and removed, `false` otherwise
1302    pub async fn remove_event_listener(&self, id: &str) -> bool {
1303        self.event_emitter.remove_listener(id).await
1304    }
1305
1306    /// Stops the SDK's background tasks
1307    ///
1308    /// This method stops the background tasks started by the `start()` method.
1309    /// It should be called before your application terminates to ensure proper cleanup.
1310    ///
1311    /// # Returns
1312    ///
1313    /// Result containing either success or an `SdkError` if the background task couldn't be stopped
1314    pub async fn disconnect(&self) -> Result<(), SdkError> {
1315        info!("Disconnecting Breez SDK");
1316        self.shutdown_sender
1317            .send(())
1318            .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
1319
1320        self.shutdown_sender.closed().await;
1321        info!("Breez SDK disconnected");
1322        Ok(())
1323    }
1324
1325    pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
1326        parse_input(input, Some(self.external_input_parsers.clone())).await
1327    }
1328
1329    /// Returns the balance of the wallet in satoshis
1330    #[allow(unused_variables)]
1331    pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
1332        if request.ensure_synced.unwrap_or_default() {
1333            self.initial_synced_watcher
1334                .clone()
1335                .changed()
1336                .await
1337                .map_err(|_| {
1338                    SdkError::Generic("Failed to receive initial synced signal".to_string())
1339                })?;
1340        }
1341        let object_repository = ObjectCacheRepository::new(self.storage.clone());
1342        let account_info = object_repository
1343            .fetch_account_info()
1344            .await?
1345            .unwrap_or_default();
1346        Ok(GetInfoResponse {
1347            balance_sats: account_info.balance_sats,
1348            token_balances: account_info.token_balances,
1349        })
1350    }
1351
1352    pub async fn receive_payment(
1353        &self,
1354        request: ReceivePaymentRequest,
1355    ) -> Result<ReceivePaymentResponse, SdkError> {
1356        self.ensure_spark_private_mode_initialized().await?;
1357        match request.payment_method {
1358            ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse {
1359                fee: 0,
1360                payment_request: self
1361                    .spark_wallet
1362                    .get_spark_address()?
1363                    .to_address_string()
1364                    .map_err(|e| {
1365                        SdkError::Generic(format!("Failed to convert Spark address to string: {e}"))
1366                    })?,
1367            }),
1368            ReceivePaymentMethod::SparkInvoice {
1369                amount,
1370                token_identifier,
1371                expiry_time,
1372                description,
1373                sender_public_key,
1374            } => {
1375                let invoice = self
1376                    .spark_wallet
1377                    .create_spark_invoice(
1378                        amount,
1379                        token_identifier.clone(),
1380                        expiry_time
1381                            .map(|time| {
1382                                SystemTime::UNIX_EPOCH
1383                                    .checked_add(Duration::from_secs(time))
1384                                    .ok_or(SdkError::Generic("Invalid expiry time".to_string()))
1385                            })
1386                            .transpose()?,
1387                        description,
1388                        sender_public_key.map(|key| PublicKey::from_str(&key).unwrap()),
1389                    )
1390                    .await?;
1391                Ok(ReceivePaymentResponse {
1392                    fee: 0,
1393                    payment_request: invoice,
1394                })
1395            }
1396            ReceivePaymentMethod::BitcoinAddress => {
1397                // TODO: allow passing amount
1398
1399                let object_repository = ObjectCacheRepository::new(self.storage.clone());
1400
1401                // First lookup in storage cache
1402                let static_deposit_address =
1403                    object_repository.fetch_static_deposit_address().await?;
1404                if let Some(static_deposit_address) = static_deposit_address {
1405                    return Ok(ReceivePaymentResponse {
1406                        payment_request: static_deposit_address.address.clone(),
1407                        fee: 0,
1408                    });
1409                }
1410
1411                // Then query existing addresses
1412                let deposit_addresses = self
1413                    .spark_wallet
1414                    .list_static_deposit_addresses(None)
1415                    .await?;
1416
1417                // In case there are no addresses, generate a new one and cache it
1418                let address = match deposit_addresses.items.last() {
1419                    Some(address) => address.to_string(),
1420                    None => self
1421                        .spark_wallet
1422                        .generate_deposit_address(true)
1423                        .await?
1424                        .to_string(),
1425                };
1426
1427                object_repository
1428                    .save_static_deposit_address(&StaticDepositAddress {
1429                        address: address.clone(),
1430                    })
1431                    .await?;
1432
1433                Ok(ReceivePaymentResponse {
1434                    payment_request: address,
1435                    fee: 0,
1436                })
1437            }
1438            ReceivePaymentMethod::Bolt11Invoice {
1439                description,
1440                amount_sats,
1441                expiry_secs,
1442            } => Ok(ReceivePaymentResponse {
1443                payment_request: self
1444                    .spark_wallet
1445                    .create_lightning_invoice(
1446                        amount_sats.unwrap_or_default(),
1447                        Some(InvoiceDescription::Memo(description.clone())),
1448                        None,
1449                        expiry_secs,
1450                        self.config.prefer_spark_over_lightning,
1451                    )
1452                    .await?
1453                    .invoice,
1454                fee: 0,
1455            }),
1456        }
1457    }
1458
1459    pub async fn claim_htlc_payment(
1460        &self,
1461        request: ClaimHtlcPaymentRequest,
1462    ) -> Result<ClaimHtlcPaymentResponse, SdkError> {
1463        let preimage = Preimage::from_hex(&request.preimage)
1464            .map_err(|_| SdkError::InvalidInput("Invalid preimage".to_string()))?;
1465        let payment_hash = preimage.compute_hash();
1466
1467        // Check if there is a claimable HTLC with the given payment hash
1468        let claimable_htlc_transfers = self
1469            .spark_wallet
1470            .list_claimable_htlc_transfers(None)
1471            .await?;
1472        if !claimable_htlc_transfers
1473            .iter()
1474            .filter_map(|t| t.htlc_preimage_request.as_ref())
1475            .any(|p| p.payment_hash == payment_hash)
1476        {
1477            return Err(SdkError::InvalidInput(
1478                "No claimable HTLC with the given payment hash".to_string(),
1479            ));
1480        }
1481
1482        let transfer = self.spark_wallet.claim_htlc(&preimage).await?;
1483        let payment: Payment = transfer.try_into()?;
1484
1485        // Insert the payment into storage to make it immediately available for listing
1486        self.storage.insert_payment(payment.clone()).await?;
1487
1488        Ok(ClaimHtlcPaymentResponse { payment })
1489    }
1490
1491    pub async fn prepare_lnurl_pay(
1492        &self,
1493        request: PrepareLnurlPayRequest,
1494    ) -> Result<PrepareLnurlPayResponse, SdkError> {
1495        let success_data = match validate_lnurl_pay(
1496            self.lnurl_client.as_ref(),
1497            request.amount_sats.saturating_mul(1_000),
1498            &None,
1499            &request.pay_request.clone().into(),
1500            self.config.network.into(),
1501            request.validate_success_action_url,
1502        )
1503        .await?
1504        {
1505            lnurl::pay::ValidatedCallbackResponse::EndpointError { data } => {
1506                return Err(LnurlError::EndpointError(data.reason).into());
1507            }
1508            lnurl::pay::ValidatedCallbackResponse::EndpointSuccess { data } => data,
1509        };
1510
1511        let prepare_response = self
1512            .prepare_send_payment(PrepareSendPaymentRequest {
1513                payment_request: success_data.pr,
1514                amount: Some(request.amount_sats.into()),
1515                token_identifier: None,
1516                conversion_options: None,
1517            })
1518            .await?;
1519
1520        let SendPaymentMethod::Bolt11Invoice {
1521            invoice_details,
1522            lightning_fee_sats,
1523            ..
1524        } = prepare_response.payment_method
1525        else {
1526            return Err(SdkError::Generic(
1527                "Expected Bolt11Invoice payment method".to_string(),
1528            ));
1529        };
1530
1531        Ok(PrepareLnurlPayResponse {
1532            amount_sats: request.amount_sats,
1533            comment: request.comment,
1534            pay_request: request.pay_request,
1535            invoice_details,
1536            fee_sats: lightning_fee_sats,
1537            success_action: success_data.success_action.map(From::from),
1538        })
1539    }
1540
1541    pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
1542        self.ensure_spark_private_mode_initialized().await?;
1543        let mut payment = Box::pin(self.maybe_convert_token_send_payment(
1544            SendPaymentRequest {
1545                prepare_response: PrepareSendPaymentResponse {
1546                    payment_method: SendPaymentMethod::Bolt11Invoice {
1547                        invoice_details: request.prepare_response.invoice_details,
1548                        spark_transfer_fee_sats: None,
1549                        lightning_fee_sats: request.prepare_response.fee_sats,
1550                    },
1551                    amount: request.prepare_response.amount_sats.into(),
1552                    token_identifier: None,
1553                    conversion_estimate: None,
1554                },
1555                options: None,
1556                idempotency_key: request.idempotency_key,
1557            },
1558            true,
1559        ))
1560        .await?
1561        .payment;
1562
1563        let success_action = process_success_action(
1564            &payment,
1565            request
1566                .prepare_response
1567                .success_action
1568                .clone()
1569                .map(Into::into)
1570                .as_ref(),
1571        )?;
1572
1573        let lnurl_info = LnurlPayInfo {
1574            ln_address: request.prepare_response.pay_request.address,
1575            comment: request.prepare_response.comment,
1576            domain: Some(request.prepare_response.pay_request.domain),
1577            metadata: Some(request.prepare_response.pay_request.metadata_str),
1578            processed_success_action: success_action.clone().map(From::from),
1579            raw_success_action: request.prepare_response.success_action,
1580        };
1581        let Some(PaymentDetails::Lightning {
1582            lnurl_pay_info,
1583            description,
1584            ..
1585        }) = &mut payment.details
1586        else {
1587            return Err(SdkError::Generic(
1588                "Expected Lightning payment details".to_string(),
1589            ));
1590        };
1591        *lnurl_pay_info = Some(lnurl_info.clone());
1592
1593        let lnurl_description = lnurl_info.extract_description();
1594        description.clone_from(&lnurl_description);
1595
1596        self.storage
1597            .set_payment_metadata(
1598                payment.id.clone(),
1599                PaymentMetadata {
1600                    lnurl_pay_info: Some(lnurl_info),
1601                    lnurl_description,
1602                    ..Default::default()
1603                },
1604            )
1605            .await?;
1606
1607        self.event_emitter
1608            .emit(&SdkEvent::from_payment(payment.clone()))
1609            .await;
1610        Ok(LnurlPayResponse {
1611            payment,
1612            success_action: success_action.map(From::from),
1613        })
1614    }
1615
1616    /// Performs an LNURL withdraw operation for the amount of satoshis to
1617    /// withdraw and the LNURL withdraw request details. The LNURL withdraw request
1618    /// details can be obtained from calling [`BreezSdk::parse`].
1619    ///
1620    /// The method generates a Lightning invoice for the withdraw amount, stores
1621    /// the LNURL withdraw metadata, and performs the LNURL withdraw using  the generated
1622    /// invoice.
1623    ///
1624    /// If the `completion_timeout_secs` parameter is provided and greater than 0, the
1625    /// method will wait for the payment to be completed within that period. If the
1626    /// withdraw is completed within the timeout, the `payment` field in the response
1627    /// will be set with the payment details. If the `completion_timeout_secs`
1628    /// parameter is not provided or set to 0, the method will not wait for the payment
1629    /// to be completed. If the withdraw is not completed within the
1630    /// timeout, the `payment` field will be empty.
1631    ///
1632    /// # Arguments
1633    ///
1634    /// * `request` - The LNURL withdraw request
1635    ///
1636    /// # Returns
1637    ///
1638    /// Result containing either:
1639    /// * `LnurlWithdrawResponse` - The payment details if the withdraw request was successful
1640    /// * `SdkError` - If there was an error during the withdraw process
1641    pub async fn lnurl_withdraw(
1642        &self,
1643        request: LnurlWithdrawRequest,
1644    ) -> Result<LnurlWithdrawResponse, SdkError> {
1645        self.ensure_spark_private_mode_initialized().await?;
1646        let LnurlWithdrawRequest {
1647            amount_sats,
1648            withdraw_request,
1649            completion_timeout_secs,
1650        } = request;
1651        let withdraw_request: breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails =
1652            withdraw_request.into();
1653        if !withdraw_request.is_amount_valid(amount_sats) {
1654            return Err(SdkError::InvalidInput(
1655                "Amount must be within min/max LNURL withdrawable limits".to_string(),
1656            ));
1657        }
1658
1659        // Generate a Lightning invoice for the withdraw
1660        let payment_request = self
1661            .receive_payment(ReceivePaymentRequest {
1662                payment_method: ReceivePaymentMethod::Bolt11Invoice {
1663                    description: withdraw_request.default_description.clone(),
1664                    amount_sats: Some(amount_sats),
1665                    expiry_secs: None,
1666                },
1667            })
1668            .await?
1669            .payment_request;
1670
1671        // Store the LNURL withdraw metadata before executing the withdraw
1672        let cache = ObjectCacheRepository::new(self.storage.clone());
1673        cache
1674            .save_payment_metadata(
1675                &payment_request,
1676                &PaymentMetadata {
1677                    lnurl_withdraw_info: Some(LnurlWithdrawInfo {
1678                        withdraw_url: withdraw_request.callback.clone(),
1679                    }),
1680                    lnurl_description: Some(withdraw_request.default_description.clone()),
1681                    ..Default::default()
1682                },
1683            )
1684            .await?;
1685
1686        // Perform the LNURL withdraw using the generated invoice
1687        let withdraw_response = execute_lnurl_withdraw(
1688            self.lnurl_client.as_ref(),
1689            &withdraw_request,
1690            &payment_request,
1691        )
1692        .await?;
1693        if let lnurl::withdraw::ValidatedCallbackResponse::EndpointError { data } =
1694            withdraw_response
1695        {
1696            return Err(LnurlError::EndpointError(data.reason).into());
1697        }
1698
1699        let completion_timeout_secs = match completion_timeout_secs {
1700            Some(secs) if secs > 0 => secs,
1701            _ => {
1702                return Ok(LnurlWithdrawResponse {
1703                    payment_request,
1704                    payment: None,
1705                });
1706            }
1707        };
1708
1709        // Wait for the payment to be completed
1710        let payment = self
1711            .wait_for_payment(
1712                WaitForPaymentIdentifier::PaymentRequest(payment_request.clone()),
1713                completion_timeout_secs,
1714            )
1715            .await
1716            .ok();
1717        Ok(LnurlWithdrawResponse {
1718            payment_request,
1719            payment,
1720        })
1721    }
1722
1723    /// Performs LNURL-auth with the service.
1724    ///
1725    /// This method implements the LNURL-auth protocol as specified in LUD-04 and LUD-05.
1726    /// It derives a domain-specific linking key, signs the challenge, and sends the
1727    /// authentication request to the service.
1728    ///
1729    /// # Arguments
1730    ///
1731    /// * `request_data` - The parsed LNURL-auth request details obtained from [`parse`]
1732    ///
1733    /// # Returns
1734    ///
1735    /// * `Ok(LnurlCallbackStatus::Ok)` - Authentication was successful
1736    /// * `Ok(LnurlCallbackStatus::ErrorStatus{reason})` - Service returned an error
1737    /// * `Err(SdkError)` - An error occurred during the authentication process
1738    ///
1739    /// # Example
1740    ///
1741    /// ```rust,no_run
1742    /// # use breez_sdk_spark::{BreezSdk, InputType};
1743    /// # async fn example(sdk: BreezSdk) -> Result<(), Box<dyn std::error::Error>> {
1744    /// // 1. Parse the LNURL-auth string
1745    /// let input = sdk.parse("lnurl1...").await?;
1746    /// let auth_request = match input {
1747    ///     InputType::LnurlAuth(data) => data,
1748    ///     _ => return Err("Not an auth request".into()),
1749    /// };
1750    ///
1751    /// // 2. Show user the domain and get confirmation
1752    /// println!("Authenticate with {}?", auth_request.domain);
1753    ///
1754    /// // 3. Perform authentication
1755    /// let status = sdk.lnurl_auth(auth_request).await?;
1756    /// match status {
1757    ///     breez_sdk_spark::LnurlCallbackStatus::Ok => println!("Success!"),
1758    ///     breez_sdk_spark::LnurlCallbackStatus::ErrorStatus { error_details } => {
1759    ///         println!("Error: {}", error_details.reason)
1760    ///     }
1761    /// }
1762    /// # Ok(())
1763    /// # }
1764    /// ```
1765    ///
1766    /// # See Also
1767    ///
1768    /// * LUD-04: <https://github.com/lnurl/luds/blob/luds/04.md>
1769    /// * LUD-05: <https://github.com/lnurl/luds/blob/luds/05.md>
1770    pub async fn lnurl_auth(
1771        &self,
1772        request_data: LnurlAuthRequestDetails,
1773    ) -> Result<LnurlCallbackStatus, SdkError> {
1774        let request: breez_sdk_common::lnurl::auth::LnurlAuthRequestDetails = request_data.into();
1775        let status = breez_sdk_common::lnurl::auth::perform_lnurl_auth(
1776            self.lnurl_client.as_ref(),
1777            &request,
1778            self.lnurl_auth_signer.as_ref(),
1779        )
1780        .await
1781        .map_err(|e| match e {
1782            LnurlError::ServiceConnectivity(msg) => SdkError::NetworkError(msg.to_string()),
1783            LnurlError::InvalidUri(msg) => SdkError::InvalidInput(msg),
1784            _ => SdkError::Generic(e.to_string()),
1785        })?;
1786        Ok(status.into())
1787    }
1788
1789    #[allow(clippy::too_many_lines)]
1790    pub async fn prepare_send_payment(
1791        &self,
1792        request: PrepareSendPaymentRequest,
1793    ) -> Result<PrepareSendPaymentResponse, SdkError> {
1794        let parsed_input = self.parse(&request.payment_request).await?;
1795
1796        validate_prepare_send_payment_request(
1797            &parsed_input,
1798            &request,
1799            &self.spark_wallet.get_identity_public_key().to_string(),
1800        )?;
1801
1802        match &parsed_input {
1803            InputType::SparkAddress(spark_address_details) => {
1804                let amount = request
1805                    .amount
1806                    .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1807                let conversion_estimate = self
1808                    .estimate_conversion(
1809                        request.conversion_options.as_ref(),
1810                        request.token_identifier.as_ref(),
1811                        amount,
1812                    )
1813                    .await?;
1814
1815                Ok(PrepareSendPaymentResponse {
1816                    payment_method: SendPaymentMethod::SparkAddress {
1817                        address: spark_address_details.address.clone(),
1818                        fee: 0,
1819                        token_identifier: request.token_identifier.clone(),
1820                    },
1821                    amount,
1822                    token_identifier: request.token_identifier,
1823                    conversion_estimate,
1824                })
1825            }
1826            InputType::SparkInvoice(spark_invoice_details) => {
1827                let amount = spark_invoice_details
1828                    .amount
1829                    .or(request.amount)
1830                    .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1831                let conversion_estimate = self
1832                    .estimate_conversion(
1833                        request.conversion_options.as_ref(),
1834                        request.token_identifier.as_ref(),
1835                        amount,
1836                    )
1837                    .await?;
1838
1839                Ok(PrepareSendPaymentResponse {
1840                    payment_method: SendPaymentMethod::SparkInvoice {
1841                        spark_invoice_details: spark_invoice_details.clone(),
1842                        fee: 0,
1843                        token_identifier: request.token_identifier.clone(),
1844                    },
1845                    amount,
1846                    token_identifier: request.token_identifier,
1847                    conversion_estimate,
1848                })
1849            }
1850            InputType::Bolt11Invoice(detailed_bolt11_invoice) => {
1851                let spark_address: Option<SparkAddress> = self
1852                    .spark_wallet
1853                    .extract_spark_address(&request.payment_request)?;
1854
1855                let spark_transfer_fee_sats = if spark_address.is_some() {
1856                    Some(0)
1857                } else {
1858                    None
1859                };
1860
1861                let amount = request
1862                    .amount
1863                    .or(detailed_bolt11_invoice
1864                        .amount_msat
1865                        .map(|msat| u128::from(msat).saturating_div(1000)))
1866                    .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1867                let lightning_fee_sats = self
1868                    .spark_wallet
1869                    .fetch_lightning_send_fee_estimate(
1870                        &request.payment_request,
1871                        request
1872                            .amount
1873                            .map(|a| Ok::<u64, SdkError>(a.try_into()?))
1874                            .transpose()?,
1875                    )
1876                    .await?;
1877                let conversion_estimate = self
1878                    .estimate_conversion(
1879                        request.conversion_options.as_ref(),
1880                        request.token_identifier.as_ref(),
1881                        amount.saturating_add(u128::from(lightning_fee_sats)),
1882                    )
1883                    .await?;
1884
1885                Ok(PrepareSendPaymentResponse {
1886                    payment_method: SendPaymentMethod::Bolt11Invoice {
1887                        invoice_details: detailed_bolt11_invoice.clone(),
1888                        spark_transfer_fee_sats,
1889                        lightning_fee_sats,
1890                    },
1891                    amount,
1892                    token_identifier: request.token_identifier,
1893                    conversion_estimate,
1894                })
1895            }
1896            InputType::BitcoinAddress(withdrawal_address) => {
1897                let amount = request
1898                    .amount
1899                    .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1900                let fee_quote: SendOnchainFeeQuote = self
1901                    .spark_wallet
1902                    .fetch_coop_exit_fee_quote(
1903                        &withdrawal_address.address,
1904                        Some(amount.try_into()?),
1905                    )
1906                    .await?
1907                    .into();
1908                let conversion_estimate = self
1909                    .estimate_conversion(
1910                        request.conversion_options.as_ref(),
1911                        request.token_identifier.as_ref(),
1912                        amount.saturating_add(u128::from(fee_quote.speed_fast.total_fee_sat())),
1913                    )
1914                    .await?;
1915                Ok(PrepareSendPaymentResponse {
1916                    payment_method: SendPaymentMethod::BitcoinAddress {
1917                        address: withdrawal_address.clone(),
1918                        fee_quote,
1919                    },
1920                    amount,
1921                    token_identifier: None,
1922                    conversion_estimate,
1923                })
1924            }
1925            _ => Err(SdkError::InvalidInput(
1926                "Unsupported payment method".to_string(),
1927            )),
1928        }
1929    }
1930
1931    pub async fn send_payment(
1932        &self,
1933        request: SendPaymentRequest,
1934    ) -> Result<SendPaymentResponse, SdkError> {
1935        self.ensure_spark_private_mode_initialized().await?;
1936        Box::pin(self.maybe_convert_token_send_payment(request, false)).await
1937    }
1938
1939    pub async fn fetch_conversion_limits(
1940        &self,
1941        request: FetchConversionLimitsRequest,
1942    ) -> Result<FetchConversionLimitsResponse, SdkError> {
1943        let (asset_in_address, asset_out_address) = request
1944            .conversion_type
1945            .as_asset_addresses(request.token_identifier.as_ref())?;
1946        let min_amounts = self
1947            .flashnet_client
1948            .get_min_amounts(GetMinAmountsRequest {
1949                asset_in_address,
1950                asset_out_address,
1951            })
1952            .await?;
1953        Ok(FetchConversionLimitsResponse {
1954            min_from_amount: min_amounts.asset_in_min,
1955            min_to_amount: min_amounts.asset_out_min,
1956        })
1957    }
1958
1959    /// Synchronizes the wallet with the Spark network
1960    #[allow(unused_variables)]
1961    pub async fn sync_wallet(
1962        &self,
1963        request: SyncWalletRequest,
1964    ) -> Result<SyncWalletResponse, SdkError> {
1965        let (tx, rx) = oneshot::channel();
1966
1967        if let Err(e) = self.sync_trigger.send(SyncRequest::full(Some(tx))) {
1968            error!("Failed to send sync trigger: {e:?}");
1969        }
1970        let _ = rx.await.map_err(|e| {
1971            error!("Failed to receive sync trigger: {e:?}");
1972            SdkError::Generic(format!("sync trigger failed: {e:?}"))
1973        })?;
1974        Ok(SyncWalletResponse {})
1975    }
1976
1977    /// Lists payments from the storage with pagination
1978    ///
1979    /// This method provides direct access to the payment history stored in the database.
1980    /// It returns payments in reverse chronological order (newest first).
1981    ///
1982    /// # Arguments
1983    ///
1984    /// * `request` - Contains pagination parameters (offset and limit)
1985    ///
1986    /// # Returns
1987    ///
1988    /// * `Ok(ListPaymentsResponse)` - Contains the list of payments if successful
1989    /// * `Err(SdkError)` - If there was an error accessing the storage
1990    ///
1991    pub async fn list_payments(
1992        &self,
1993        request: ListPaymentsRequest,
1994    ) -> Result<ListPaymentsResponse, SdkError> {
1995        let payments = self.storage.list_payments(request).await?;
1996        Ok(ListPaymentsResponse { payments })
1997    }
1998
1999    pub async fn get_payment(
2000        &self,
2001        request: GetPaymentRequest,
2002    ) -> Result<GetPaymentResponse, SdkError> {
2003        let payment = self.storage.get_payment_by_id(request.payment_id).await?;
2004        Ok(GetPaymentResponse { payment })
2005    }
2006
2007    pub async fn claim_deposit(
2008        &self,
2009        request: ClaimDepositRequest,
2010    ) -> Result<ClaimDepositResponse, SdkError> {
2011        self.ensure_spark_private_mode_initialized().await?;
2012        let detailed_utxo =
2013            CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
2014                .fetch_detailed_utxo(&request.txid, request.vout)
2015                .await?;
2016
2017        let max_fee = request
2018            .max_fee
2019            .or(self.config.max_deposit_claim_fee.clone());
2020        match self.claim_utxo(&detailed_utxo, max_fee).await {
2021            Ok(transfer) => {
2022                self.storage
2023                    .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
2024                    .await?;
2025                if let Err(e) = self
2026                    .sync_trigger
2027                    .send(SyncRequest::no_reply(SyncType::WalletState))
2028                {
2029                    error!("Failed to execute sync after deposit claim: {e:?}");
2030                }
2031                Ok(ClaimDepositResponse {
2032                    payment: transfer.try_into()?,
2033                })
2034            }
2035            Err(e) => {
2036                error!("Failed to claim deposit: {e:?}");
2037                self.storage
2038                    .update_deposit(
2039                        detailed_utxo.txid.to_string(),
2040                        detailed_utxo.vout,
2041                        UpdateDepositPayload::ClaimError {
2042                            error: e.clone().into(),
2043                        },
2044                    )
2045                    .await?;
2046                Err(e)
2047            }
2048        }
2049    }
2050
2051    pub async fn refund_deposit(
2052        &self,
2053        request: RefundDepositRequest,
2054    ) -> Result<RefundDepositResponse, SdkError> {
2055        let detailed_utxo =
2056            CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
2057                .fetch_detailed_utxo(&request.txid, request.vout)
2058                .await?;
2059        let tx = self
2060            .spark_wallet
2061            .refund_static_deposit(
2062                detailed_utxo.clone().tx,
2063                Some(detailed_utxo.vout),
2064                &request.destination_address,
2065                request.fee.into(),
2066            )
2067            .await?;
2068        let deposit: DepositInfo = detailed_utxo.into();
2069        let tx_hex = serialize(&tx).as_hex().to_string();
2070        let tx_id = tx.compute_txid().as_raw_hash().to_string();
2071
2072        // Store the refund transaction details separately
2073        self.storage
2074            .update_deposit(
2075                deposit.txid.clone(),
2076                deposit.vout,
2077                UpdateDepositPayload::Refund {
2078                    refund_tx: tx_hex.clone(),
2079                    refund_txid: tx_id.clone(),
2080                },
2081            )
2082            .await?;
2083
2084        self.chain_service
2085            .broadcast_transaction(tx_hex.clone())
2086            .await?;
2087        Ok(RefundDepositResponse { tx_id, tx_hex })
2088    }
2089
2090    #[allow(unused_variables)]
2091    pub async fn list_unclaimed_deposits(
2092        &self,
2093        request: ListUnclaimedDepositsRequest,
2094    ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
2095        let deposits = self.storage.list_deposits().await?;
2096        Ok(ListUnclaimedDepositsResponse { deposits })
2097    }
2098
2099    pub async fn check_lightning_address_available(
2100        &self,
2101        req: CheckLightningAddressRequest,
2102    ) -> Result<bool, SdkError> {
2103        let Some(client) = &self.lnurl_server_client else {
2104            return Err(SdkError::Generic(
2105                "LNURL server is not configured".to_string(),
2106            ));
2107        };
2108
2109        let username = sanitize_username(&req.username);
2110        let available = client.check_username_available(&username).await?;
2111        Ok(available)
2112    }
2113
2114    pub async fn get_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
2115        let cache = ObjectCacheRepository::new(self.storage.clone());
2116        Ok(cache.fetch_lightning_address().await?)
2117    }
2118
2119    pub async fn register_lightning_address(
2120        &self,
2121        request: RegisterLightningAddressRequest,
2122    ) -> Result<LightningAddressInfo, SdkError> {
2123        // Ensure spark private mode is initialized before registering
2124        self.ensure_spark_private_mode_initialized().await?;
2125
2126        self.register_lightning_address_internal(request).await
2127    }
2128
2129    pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
2130        let cache = ObjectCacheRepository::new(self.storage.clone());
2131        let Some(address_info) = cache.fetch_lightning_address().await? else {
2132            return Ok(());
2133        };
2134
2135        let Some(client) = &self.lnurl_server_client else {
2136            return Err(SdkError::Generic(
2137                "LNURL server is not configured".to_string(),
2138            ));
2139        };
2140
2141        let params = crate::lnurl::UnregisterLightningAddressRequest {
2142            username: address_info.username,
2143        };
2144
2145        client.unregister_lightning_address(&params).await?;
2146        cache.delete_lightning_address().await?;
2147        Ok(())
2148    }
2149
2150    /// List fiat currencies for which there is a known exchange rate,
2151    /// sorted by the canonical name of the currency.
2152    pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
2153        let currencies = self
2154            .fiat_service
2155            .fetch_fiat_currencies()
2156            .await?
2157            .into_iter()
2158            .map(From::from)
2159            .collect();
2160        Ok(ListFiatCurrenciesResponse { currencies })
2161    }
2162
2163    /// List the latest rates of fiat currencies, sorted by name.
2164    pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
2165        let rates = self
2166            .fiat_service
2167            .fetch_fiat_rates()
2168            .await?
2169            .into_iter()
2170            .map(From::from)
2171            .collect();
2172        Ok(ListFiatRatesResponse { rates })
2173    }
2174
2175    /// Get the recommended BTC fees based on the configured chain service.
2176    pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
2177        Ok(self.chain_service.recommended_fees().await?)
2178    }
2179
2180    /// Returns the metadata for the given token identifiers.
2181    ///
2182    /// Results are not guaranteed to be in the same order as the input token identifiers.    
2183    ///
2184    /// If the metadata is not found locally in cache, it will be queried from
2185    /// the Spark network and then cached.
2186    pub async fn get_tokens_metadata(
2187        &self,
2188        request: GetTokensMetadataRequest,
2189    ) -> Result<GetTokensMetadataResponse, SdkError> {
2190        let metadata = get_tokens_metadata_cached_or_query(
2191            &self.spark_wallet,
2192            &ObjectCacheRepository::new(self.storage.clone()),
2193            &request
2194                .token_identifiers
2195                .iter()
2196                .map(String::as_str)
2197                .collect::<Vec<_>>(),
2198        )
2199        .await?;
2200        Ok(GetTokensMetadataResponse {
2201            tokens_metadata: metadata,
2202        })
2203    }
2204
2205    /// Signs a message with the wallet's identity key. The message is SHA256
2206    /// hashed before signing. The returned signature will be hex encoded in
2207    /// DER format by default, or compact format if specified.
2208    pub async fn sign_message(
2209        &self,
2210        request: SignMessageRequest,
2211    ) -> Result<SignMessageResponse, SdkError> {
2212        let pubkey = self.spark_wallet.get_identity_public_key().to_string();
2213        let signature = self.spark_wallet.sign_message(&request.message).await?;
2214        let signature_hex = if request.compact {
2215            signature.serialize_compact().to_lower_hex_string()
2216        } else {
2217            signature.serialize_der().to_lower_hex_string()
2218        };
2219
2220        Ok(SignMessageResponse {
2221            pubkey,
2222            signature: signature_hex,
2223        })
2224    }
2225
2226    /// Verifies a message signature against the provided public key. The message
2227    /// is SHA256 hashed before verification. The signature can be hex encoded
2228    /// in either DER or compact format.
2229    pub async fn check_message(
2230        &self,
2231        request: CheckMessageRequest,
2232    ) -> Result<CheckMessageResponse, SdkError> {
2233        let pubkey = PublicKey::from_str(&request.pubkey)
2234            .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
2235        let signature_bytes = hex::decode(&request.signature)
2236            .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
2237        let signature = Signature::from_der(&signature_bytes)
2238            .or_else(|_| Signature::from_compact(&signature_bytes))
2239            .map_err(|_| {
2240                SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
2241            })?;
2242
2243        let is_valid = self
2244            .spark_wallet
2245            .verify_message(&request.message, &signature, &pubkey)
2246            .await
2247            .is_ok();
2248        Ok(CheckMessageResponse { is_valid })
2249    }
2250
2251    /// Returns the user settings for the wallet.
2252    ///
2253    /// Some settings are fetched from the Spark network so network requests are performed.
2254    pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
2255        // Ensure spark private mode is initialized to avoid race conditions with the initialization task.
2256        self.ensure_spark_private_mode_initialized().await?;
2257
2258        let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
2259
2260        // We may in the future have user settings that are stored locally and synced using real-time sync.
2261
2262        Ok(UserSettings {
2263            spark_private_mode_enabled: spark_user_settings.private_enabled,
2264        })
2265    }
2266
2267    /// Updates the user settings for the wallet.
2268    ///
2269    /// Some settings are updated on the Spark network so network requests may be performed.
2270    pub async fn update_user_settings(
2271        &self,
2272        request: UpdateUserSettingsRequest,
2273    ) -> Result<(), SdkError> {
2274        if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
2275            self.spark_wallet
2276                .update_wallet_settings(spark_private_mode_enabled)
2277                .await?;
2278
2279            // Reregister the lightning address if spark private mode changed.
2280            let lightning_address = match self.get_lightning_address().await {
2281                Ok(lightning_address) => lightning_address,
2282                Err(e) => {
2283                    error!("Failed to get lightning address during user settings update: {e:?}");
2284                    return Ok(());
2285                }
2286            };
2287            let Some(lightning_address) = lightning_address else {
2288                return Ok(());
2289            };
2290            if let Err(e) = self
2291                .register_lightning_address_internal(RegisterLightningAddressRequest {
2292                    username: lightning_address.username,
2293                    description: Some(lightning_address.description),
2294                })
2295                .await
2296            {
2297                error!("Failed to reregister lightning address during user settings update: {e:?}");
2298            }
2299        }
2300        Ok(())
2301    }
2302
2303    /// Returns an instance of the [`TokenIssuer`] for managing token issuance.
2304    pub fn get_token_issuer(&self) -> TokenIssuer {
2305        TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
2306    }
2307
2308    /// Starts leaf optimization in the background.
2309    ///
2310    /// This method spawns the optimization work in a background task and returns
2311    /// immediately. Progress is reported via events.
2312    /// If optimization is already running, no new task will be started.
2313    pub fn start_leaf_optimization(&self) {
2314        self.spark_wallet.start_leaf_optimization();
2315    }
2316
2317    /// Cancels the ongoing leaf optimization.
2318    ///
2319    /// This method cancels the ongoing optimization and waits for it to fully stop.
2320    /// The current round will complete before stopping. This method blocks
2321    /// until the optimization has fully stopped and leaves reserved for optimization
2322    /// are available again.
2323    ///
2324    /// If no optimization is running, this method returns immediately.
2325    pub async fn cancel_leaf_optimization(&self) -> Result<(), SdkError> {
2326        self.spark_wallet.cancel_leaf_optimization().await?;
2327        Ok(())
2328    }
2329
2330    /// Returns the current optimization progress snapshot.
2331    pub fn get_leaf_optimization_progress(&self) -> OptimizationProgress {
2332        self.spark_wallet.get_leaf_optimization_progress().into()
2333    }
2334}
2335
2336// Separate impl block to avoid exposing private methods to uniffi.
2337impl BreezSdk {
2338    async fn maybe_convert_token_send_payment(
2339        &self,
2340        request: SendPaymentRequest,
2341        mut suppress_payment_event: bool,
2342    ) -> Result<SendPaymentResponse, SdkError> {
2343        // Check the idempotency key is valid and payment doesn't already exist
2344        if request.idempotency_key.is_some() && request.prepare_response.token_identifier.is_some()
2345        {
2346            return Err(SdkError::InvalidInput(
2347                "Idempotency key is not supported for token payments".to_string(),
2348            ));
2349        }
2350        if let Some(idempotency_key) = &request.idempotency_key {
2351            // If an idempotency key is provided, check if a payment with that id already exists
2352            if let Ok(payment) = self
2353                .storage
2354                .get_payment_by_id(idempotency_key.clone())
2355                .await
2356            {
2357                return Ok(SendPaymentResponse { payment });
2358            }
2359        }
2360        // Perform the send payment, with conversion if requested
2361        let res = if let Some(ConversionEstimate {
2362            options: conversion_options,
2363            ..
2364        }) = &request.prepare_response.conversion_estimate
2365        {
2366            Box::pin(self.convert_token_send_payment_internal(
2367                conversion_options,
2368                &request,
2369                &mut suppress_payment_event,
2370            ))
2371            .await
2372        } else {
2373            Box::pin(self.send_payment_internal(&request)).await
2374        };
2375        // Emit payment status event and trigger wallet state sync
2376        if let Ok(response) = &res {
2377            if !suppress_payment_event {
2378                self.event_emitter
2379                    .emit(&SdkEvent::from_payment(response.payment.clone()))
2380                    .await;
2381            }
2382            if let Err(e) = self
2383                .sync_trigger
2384                .send(SyncRequest::no_reply(SyncType::WalletState))
2385            {
2386                error!("Failed to send sync trigger: {e:?}");
2387            }
2388        }
2389        res
2390    }
2391
2392    #[allow(clippy::too_many_lines)]
2393    async fn convert_token_send_payment_internal(
2394        &self,
2395        conversion_options: &ConversionOptions,
2396        request: &SendPaymentRequest,
2397        suppress_payment_event: &mut bool,
2398    ) -> Result<SendPaymentResponse, SdkError> {
2399        // Perform a conversion before sending the payment
2400        let (conversion_response, conversion_purpose) =
2401            match &request.prepare_response.payment_method {
2402                SendPaymentMethod::SparkAddress { address, .. } => {
2403                    let spark_address = address
2404                        .parse::<SparkAddress>()
2405                        .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
2406                    let conversion_purpose = if spark_address.identity_public_key
2407                        == self.spark_wallet.get_identity_public_key()
2408                    {
2409                        ConversionPurpose::SelfTransfer
2410                    } else {
2411                        ConversionPurpose::OngoingPayment {
2412                            payment_request: address.clone(),
2413                        }
2414                    };
2415                    let res = self
2416                        .convert_token(
2417                            conversion_options,
2418                            &conversion_purpose,
2419                            request.prepare_response.token_identifier.as_ref(),
2420                            request.prepare_response.amount,
2421                        )
2422                        .await?;
2423                    (res, conversion_purpose)
2424                }
2425                SendPaymentMethod::SparkInvoice {
2426                    spark_invoice_details:
2427                        SparkInvoiceDetails {
2428                            identity_public_key,
2429                            invoice,
2430                            ..
2431                        },
2432                    ..
2433                } => {
2434                    let own_identity_public_key =
2435                        self.spark_wallet.get_identity_public_key().to_string();
2436                    let conversion_purpose = if identity_public_key == &own_identity_public_key {
2437                        ConversionPurpose::SelfTransfer
2438                    } else {
2439                        ConversionPurpose::OngoingPayment {
2440                            payment_request: invoice.clone(),
2441                        }
2442                    };
2443                    let res = self
2444                        .convert_token(
2445                            conversion_options,
2446                            &conversion_purpose,
2447                            request.prepare_response.token_identifier.as_ref(),
2448                            request.prepare_response.amount,
2449                        )
2450                        .await?;
2451                    (res, conversion_purpose)
2452                }
2453                SendPaymentMethod::Bolt11Invoice {
2454                    spark_transfer_fee_sats,
2455                    lightning_fee_sats,
2456                    invoice_details,
2457                    ..
2458                } => {
2459                    let conversion_purpose = ConversionPurpose::OngoingPayment {
2460                        payment_request: invoice_details.invoice.bolt11.clone(),
2461                    };
2462                    let res = self
2463                        .convert_token_for_bolt11_invoice(
2464                            conversion_options,
2465                            *spark_transfer_fee_sats,
2466                            *lightning_fee_sats,
2467                            request,
2468                            &conversion_purpose,
2469                        )
2470                        .await?;
2471                    (res, conversion_purpose)
2472                }
2473                SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
2474                    let conversion_purpose = ConversionPurpose::OngoingPayment {
2475                        payment_request: address.address.clone(),
2476                    };
2477                    let res = self
2478                        .convert_token_for_bitcoin_address(
2479                            conversion_options,
2480                            fee_quote,
2481                            request,
2482                            &conversion_purpose,
2483                        )
2484                        .await?;
2485                    (res, conversion_purpose)
2486                }
2487            };
2488        // Trigger a wallet state sync if converting from Bitcoin to token
2489        if matches!(
2490            conversion_options.conversion_type,
2491            ConversionType::FromBitcoin
2492        ) {
2493            let _ = self
2494                .sync_trigger
2495                .send(SyncRequest::no_reply(SyncType::WalletState));
2496        }
2497        // Wait for the received conversion payment to complete
2498        let payment = self
2499            .wait_for_payment(
2500                WaitForPaymentIdentifier::PaymentId(
2501                    conversion_response.received_payment_id.clone(),
2502                ),
2503                conversion_options
2504                    .completion_timeout_secs
2505                    .unwrap_or(DEFAULT_TOKEN_CONVERSION_TIMEOUT_SECS),
2506            )
2507            .await
2508            .map_err(|e| {
2509                SdkError::Generic(format!("Timeout waiting for conversion to complete: {e}"))
2510            })?;
2511        // For self-payments, we can skip sending the actual payment
2512        if conversion_purpose == ConversionPurpose::SelfTransfer {
2513            *suppress_payment_event = true;
2514            return Ok(SendPaymentResponse { payment });
2515        }
2516        // Now send the actual payment
2517        let response = Box::pin(self.send_payment_internal(request)).await?;
2518        // Merge payment metadata to link the payments
2519        self.merge_payment_metadata(
2520            conversion_response.sent_payment_id,
2521            PaymentMetadata {
2522                parent_payment_id: Some(response.payment.id.clone()),
2523                ..Default::default()
2524            },
2525        )
2526        .await?;
2527        self.merge_payment_metadata(
2528            conversion_response.received_payment_id,
2529            PaymentMetadata {
2530                parent_payment_id: Some(response.payment.id.clone()),
2531                ..Default::default()
2532            },
2533        )
2534        .await?;
2535
2536        Ok(response)
2537    }
2538
2539    async fn send_payment_internal(
2540        &self,
2541        request: &SendPaymentRequest,
2542    ) -> Result<SendPaymentResponse, SdkError> {
2543        match &request.prepare_response.payment_method {
2544            SendPaymentMethod::SparkAddress {
2545                address,
2546                token_identifier,
2547                ..
2548            } => {
2549                self.send_spark_address(
2550                    address,
2551                    token_identifier.clone(),
2552                    request.prepare_response.amount,
2553                    request.options.as_ref(),
2554                    request.idempotency_key.clone(),
2555                )
2556                .await
2557            }
2558            SendPaymentMethod::SparkInvoice {
2559                spark_invoice_details,
2560                ..
2561            } => {
2562                self.send_spark_invoice(&spark_invoice_details.invoice, request)
2563                    .await
2564            }
2565            SendPaymentMethod::Bolt11Invoice {
2566                invoice_details,
2567                spark_transfer_fee_sats,
2568                lightning_fee_sats,
2569                ..
2570            } => {
2571                Box::pin(self.send_bolt11_invoice(
2572                    invoice_details,
2573                    *spark_transfer_fee_sats,
2574                    *lightning_fee_sats,
2575                    request,
2576                ))
2577                .await
2578            }
2579            SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
2580                self.send_bitcoin_address(address, fee_quote, request).await
2581            }
2582        }
2583    }
2584
2585    async fn send_spark_address(
2586        &self,
2587        address: &str,
2588        token_identifier: Option<String>,
2589        amount: u128,
2590        options: Option<&SendPaymentOptions>,
2591        idempotency_key: Option<String>,
2592    ) -> Result<SendPaymentResponse, SdkError> {
2593        let spark_address = address
2594            .parse::<SparkAddress>()
2595            .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
2596
2597        // If HTLC options are provided, send an HTLC transfer
2598        if let Some(SendPaymentOptions::SparkAddress { htlc_options }) = options
2599            && let Some(htlc_options) = htlc_options
2600        {
2601            if token_identifier.is_some() {
2602                return Err(SdkError::InvalidInput(
2603                    "Can't provide both token identifier and HTLC options".to_string(),
2604                ));
2605            }
2606
2607            return self
2608                .send_spark_htlc(
2609                    &spark_address,
2610                    amount.try_into()?,
2611                    htlc_options,
2612                    idempotency_key,
2613                )
2614                .await;
2615        }
2616
2617        let payment = if let Some(identifier) = token_identifier {
2618            self.send_spark_token_address(identifier, amount, spark_address)
2619                .await?
2620        } else {
2621            let transfer_id = idempotency_key
2622                .as_ref()
2623                .map(|key| TransferId::from_str(key))
2624                .transpose()?;
2625            let transfer = self
2626                .spark_wallet
2627                .transfer(amount.try_into()?, &spark_address, transfer_id)
2628                .await?;
2629            transfer.try_into()?
2630        };
2631
2632        // Insert the payment into storage to make it immediately available for listing
2633        self.storage.insert_payment(payment.clone()).await?;
2634
2635        Ok(SendPaymentResponse { payment })
2636    }
2637
2638    async fn send_spark_htlc(
2639        &self,
2640        address: &SparkAddress,
2641        amount_sat: u64,
2642        htlc_options: &SparkHtlcOptions,
2643        idempotency_key: Option<String>,
2644    ) -> Result<SendPaymentResponse, SdkError> {
2645        let payment_hash = sha256::Hash::from_str(&htlc_options.payment_hash)
2646            .map_err(|_| SdkError::InvalidInput("Invalid payment hash".to_string()))?;
2647
2648        if htlc_options.expiry_duration_secs == 0 {
2649            return Err(SdkError::InvalidInput(
2650                "Expiry duration must be greater than 0".to_string(),
2651            ));
2652        }
2653        let expiry_duration = Duration::from_secs(htlc_options.expiry_duration_secs);
2654
2655        let transfer_id = idempotency_key
2656            .as_ref()
2657            .map(|key| TransferId::from_str(key))
2658            .transpose()?;
2659        let transfer = self
2660            .spark_wallet
2661            .create_htlc(
2662                amount_sat,
2663                address,
2664                &payment_hash,
2665                expiry_duration,
2666                transfer_id,
2667            )
2668            .await?;
2669
2670        let payment: Payment = transfer.try_into()?;
2671
2672        // Insert the payment into storage to make it immediately available for listing
2673        self.storage.insert_payment(payment.clone()).await?;
2674
2675        Ok(SendPaymentResponse { payment })
2676    }
2677
2678    async fn send_spark_token_address(
2679        &self,
2680        token_identifier: String,
2681        amount: u128,
2682        receiver_address: SparkAddress,
2683    ) -> Result<Payment, SdkError> {
2684        let token_transaction = self
2685            .spark_wallet
2686            .transfer_tokens(
2687                vec![TransferTokenOutput {
2688                    token_id: token_identifier,
2689                    amount,
2690                    receiver_address: receiver_address.clone(),
2691                    spark_invoice: None,
2692                }],
2693                None,
2694                None,
2695            )
2696            .await?;
2697
2698        map_and_persist_token_transaction(&self.spark_wallet, &self.storage, &token_transaction)
2699            .await
2700    }
2701
2702    async fn send_spark_invoice(
2703        &self,
2704        invoice: &str,
2705        request: &SendPaymentRequest,
2706    ) -> Result<SendPaymentResponse, SdkError> {
2707        let transfer_id = request
2708            .idempotency_key
2709            .as_ref()
2710            .map(|key| TransferId::from_str(key))
2711            .transpose()?;
2712
2713        let payment = match self
2714            .spark_wallet
2715            .fulfill_spark_invoice(invoice, Some(request.prepare_response.amount), transfer_id)
2716            .await?
2717        {
2718            spark_wallet::FulfillSparkInvoiceResult::Transfer(wallet_transfer) => {
2719                (*wallet_transfer).try_into()?
2720            }
2721            spark_wallet::FulfillSparkInvoiceResult::TokenTransaction(token_transaction) => {
2722                map_and_persist_token_transaction(
2723                    &self.spark_wallet,
2724                    &self.storage,
2725                    &token_transaction,
2726                )
2727                .await?
2728            }
2729        };
2730
2731        // Insert the payment into storage to make it immediately available for listing
2732        self.storage.insert_payment(payment.clone()).await?;
2733
2734        Ok(SendPaymentResponse { payment })
2735    }
2736
2737    async fn send_bolt11_invoice(
2738        &self,
2739        invoice_details: &Bolt11InvoiceDetails,
2740        spark_transfer_fee_sats: Option<u64>,
2741        lightning_fee_sats: u64,
2742        request: &SendPaymentRequest,
2743    ) -> Result<SendPaymentResponse, SdkError> {
2744        let amount_to_send = match invoice_details.amount_msat {
2745            // We are not sending amount in case the invoice contains it.
2746            Some(_) => None,
2747            // We are sending amount for zero amount invoice
2748            None => Some(request.prepare_response.amount),
2749        };
2750        let (prefer_spark, completion_timeout_secs) = match request.options {
2751            Some(SendPaymentOptions::Bolt11Invoice {
2752                prefer_spark,
2753                completion_timeout_secs,
2754            }) => (prefer_spark, completion_timeout_secs),
2755            _ => (self.config.prefer_spark_over_lightning, None),
2756        };
2757        let fee_sats = match (prefer_spark, spark_transfer_fee_sats, lightning_fee_sats) {
2758            (true, Some(fee), _) => fee,
2759            _ => lightning_fee_sats,
2760        };
2761        let transfer_id = request
2762            .idempotency_key
2763            .as_ref()
2764            .map(|idempotency_key| TransferId::from_str(idempotency_key))
2765            .transpose()?;
2766
2767        let payment_response = self
2768            .spark_wallet
2769            .pay_lightning_invoice(
2770                &invoice_details.invoice.bolt11,
2771                amount_to_send
2772                    .map(|a| Ok::<u64, SdkError>(a.try_into()?))
2773                    .transpose()?,
2774                Some(fee_sats),
2775                prefer_spark,
2776                transfer_id,
2777            )
2778            .await?;
2779        let payment = match payment_response.lightning_payment {
2780            Some(lightning_payment) => {
2781                let ssp_id = lightning_payment.id.clone();
2782                let payment = Payment::from_lightning(
2783                    lightning_payment,
2784                    request.prepare_response.amount,
2785                    payment_response.transfer.id.to_string(),
2786                )?;
2787                self.poll_lightning_send_payment(&payment, ssp_id);
2788                payment
2789            }
2790            None => payment_response.transfer.try_into()?,
2791        };
2792
2793        let Some(completion_timeout_secs) = completion_timeout_secs else {
2794            return Ok(SendPaymentResponse { payment });
2795        };
2796
2797        if completion_timeout_secs == 0 {
2798            return Ok(SendPaymentResponse { payment });
2799        }
2800
2801        let payment = self
2802            .wait_for_payment(
2803                WaitForPaymentIdentifier::PaymentId(payment.id.clone()),
2804                completion_timeout_secs,
2805            )
2806            .await
2807            .unwrap_or(payment);
2808
2809        // Insert the payment into storage to make it immediately available for listing
2810        self.storage.insert_payment(payment.clone()).await?;
2811
2812        Ok(SendPaymentResponse { payment })
2813    }
2814
2815    async fn send_bitcoin_address(
2816        &self,
2817        address: &BitcoinAddressDetails,
2818        fee_quote: &SendOnchainFeeQuote,
2819        request: &SendPaymentRequest,
2820    ) -> Result<SendPaymentResponse, SdkError> {
2821        let exit_speed = match &request.options {
2822            Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) => {
2823                confirmation_speed.clone().into()
2824            }
2825            None => ExitSpeed::Fast,
2826            _ => {
2827                return Err(SdkError::InvalidInput("Invalid options".to_string()));
2828            }
2829        };
2830        let transfer_id = request
2831            .idempotency_key
2832            .as_ref()
2833            .map(|idempotency_key| TransferId::from_str(idempotency_key))
2834            .transpose()?;
2835        let response = self
2836            .spark_wallet
2837            .withdraw(
2838                &address.address,
2839                Some(request.prepare_response.amount.try_into()?),
2840                exit_speed,
2841                fee_quote.clone().into(),
2842                transfer_id,
2843            )
2844            .await?;
2845
2846        let payment: Payment = response.try_into()?;
2847
2848        self.storage.insert_payment(payment.clone()).await?;
2849
2850        Ok(SendPaymentResponse { payment })
2851    }
2852
2853    async fn wait_for_payment(
2854        &self,
2855        identifier: WaitForPaymentIdentifier,
2856        completion_timeout_secs: u32,
2857    ) -> Result<Payment, SdkError> {
2858        let (tx, mut rx) = mpsc::channel(20);
2859        let id = self
2860            .add_event_listener(Box::new(InternalEventListener::new(tx)))
2861            .await;
2862
2863        // First check if we already have the completed payment in storage
2864        let payment = match &identifier {
2865            WaitForPaymentIdentifier::PaymentId(payment_id) => self
2866                .storage
2867                .get_payment_by_id(payment_id.clone())
2868                .await
2869                .ok(),
2870            WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
2871                self.storage
2872                    .get_payment_by_invoice(payment_request.clone())
2873                    .await?
2874            }
2875        };
2876        if let Some(payment) = payment
2877            && payment.status == PaymentStatus::Completed
2878        {
2879            self.remove_event_listener(&id).await;
2880            return Ok(payment);
2881        }
2882
2883        let timeout_res = timeout(Duration::from_secs(completion_timeout_secs.into()), async {
2884            loop {
2885                let Some(event) = rx.recv().await else {
2886                    return Err(SdkError::Generic("Event channel closed".to_string()));
2887                };
2888
2889                let SdkEvent::PaymentSucceeded { payment } = event else {
2890                    continue;
2891                };
2892
2893                if is_payment_match(&payment, &identifier) {
2894                    return Ok(payment);
2895                }
2896            }
2897        })
2898        .await
2899        .map_err(|_| SdkError::Generic("Timeout waiting for payment".to_string()));
2900
2901        self.remove_event_listener(&id).await;
2902        timeout_res?
2903    }
2904
2905    async fn merge_payment_metadata(
2906        &self,
2907        payment_id: String,
2908        mut metadata: PaymentMetadata,
2909    ) -> Result<(), SdkError> {
2910        if let Some(details) = self
2911            .storage
2912            .get_payment_by_id(payment_id.clone())
2913            .await
2914            .ok()
2915            .and_then(|p| p.details)
2916        {
2917            match details {
2918                PaymentDetails::Lightning {
2919                    lnurl_pay_info,
2920                    lnurl_withdraw_info,
2921                    ..
2922                } => {
2923                    metadata.lnurl_pay_info = metadata.lnurl_pay_info.or(lnurl_pay_info);
2924                    metadata.lnurl_withdraw_info =
2925                        metadata.lnurl_withdraw_info.or(lnurl_withdraw_info);
2926                }
2927                PaymentDetails::Spark {
2928                    conversion_info, ..
2929                }
2930                | PaymentDetails::Token {
2931                    conversion_info, ..
2932                } => {
2933                    metadata.conversion_info = metadata.conversion_info.or(conversion_info);
2934                }
2935                _ => {}
2936            }
2937        }
2938        self.storage
2939            .set_payment_metadata(payment_id, metadata)
2940            .await?;
2941        Ok(())
2942    }
2943
2944    // Pools the lightning send payment untill it is in completed state.
2945    fn poll_lightning_send_payment(&self, payment: &Payment, ssp_id: String) {
2946        const MAX_POLL_ATTEMPTS: u32 = 20;
2947        let payment_id = payment.id.clone();
2948        info!("Polling lightning send payment {}", payment_id);
2949
2950        let spark_wallet = self.spark_wallet.clone();
2951        let sync_trigger = self.sync_trigger.clone();
2952        let event_emitter = self.event_emitter.clone();
2953        let payment = payment.clone();
2954        let payment_id = payment_id.clone();
2955        let mut shutdown = self.shutdown_sender.subscribe();
2956
2957        tokio::spawn(async move {
2958            for i in 0..MAX_POLL_ATTEMPTS {
2959                info!(
2960                    "Polling lightning send payment {} attempt {}",
2961                    payment_id, i
2962                );
2963                select! {
2964                    _ = shutdown.changed() => {
2965                        info!("Shutdown signal received");
2966                        return;
2967                    },
2968                    p = spark_wallet.fetch_lightning_send_payment(&ssp_id) => {
2969                        if let Ok(Some(p)) = p && let Ok(payment) = Payment::from_lightning(p.clone(), payment.amount, payment.id.clone()) {
2970                            info!("Polling payment status = {} {:?}", payment.status, p.status);
2971                            if payment.status != PaymentStatus::Pending {
2972                                info!("Polling payment completed status = {}", payment.status);
2973                                event_emitter.emit(&SdkEvent::from_payment(payment.clone())).await;
2974                                if let Err(e) = sync_trigger.send(SyncRequest::no_reply(SyncType::WalletState)) {
2975                                    error!("Failed to send sync trigger: {e:?}");
2976                                }
2977                                return;
2978                            }
2979                        }
2980
2981                        let sleep_time = if i < 5 {
2982                            Duration::from_secs(1)
2983                        } else {
2984                            Duration::from_secs(i.into())
2985                        };
2986                        tokio::time::sleep(sleep_time).await;
2987                    }
2988                }
2989            }
2990        });
2991    }
2992
2993    /// Attempts to recover a lightning address from the lnurl server.
2994    async fn recover_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
2995        let cache = ObjectCacheRepository::new(self.storage.clone());
2996
2997        let Some(client) = &self.lnurl_server_client else {
2998            return Err(SdkError::Generic(
2999                "LNURL server is not configured".to_string(),
3000            ));
3001        };
3002        let resp = client.recover_lightning_address().await?;
3003
3004        let result = if let Some(resp) = resp {
3005            let address_info = resp.into();
3006            cache.save_lightning_address(&address_info).await?;
3007            Some(address_info)
3008        } else {
3009            cache.delete_lightning_address().await?;
3010            None
3011        };
3012
3013        Ok(result)
3014    }
3015
3016    async fn register_lightning_address_internal(
3017        &self,
3018        request: RegisterLightningAddressRequest,
3019    ) -> Result<LightningAddressInfo, SdkError> {
3020        let cache = ObjectCacheRepository::new(self.storage.clone());
3021        let Some(client) = &self.lnurl_server_client else {
3022            return Err(SdkError::Generic(
3023                "LNURL server is not configured".to_string(),
3024            ));
3025        };
3026
3027        let username = sanitize_username(&request.username);
3028
3029        let description = match request.description {
3030            Some(description) => description,
3031            None => format!("Pay to {}@{}", username, client.domain()),
3032        };
3033
3034        // Query settings directly from spark wallet to avoid recursion through get_user_settings()
3035        let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
3036        let nostr_pubkey = if spark_user_settings.private_enabled {
3037            Some(self.nostr_client.nostr_pubkey())
3038        } else {
3039            None
3040        };
3041
3042        let params = crate::lnurl::RegisterLightningAddressRequest {
3043            username: username.clone(),
3044            description: description.clone(),
3045            nostr_pubkey,
3046        };
3047
3048        let response = client.register_lightning_address(&params).await?;
3049        let address_info = LightningAddressInfo {
3050            lightning_address: response.lightning_address,
3051            description,
3052            lnurl: response.lnurl,
3053            username,
3054        };
3055        cache.save_lightning_address(&address_info).await?;
3056        Ok(address_info)
3057    }
3058
3059    async fn convert_token_for_bolt11_invoice(
3060        &self,
3061        conversion_options: &ConversionOptions,
3062        spark_transfer_fee_sats: Option<u64>,
3063        lightning_fee_sats: u64,
3064        request: &SendPaymentRequest,
3065        conversion_purpose: &ConversionPurpose,
3066    ) -> Result<TokenConversionResponse, SdkError> {
3067        // Determine the fee to be used based on preference
3068        let fee_sats = match request.options {
3069            Some(SendPaymentOptions::Bolt11Invoice { prefer_spark, .. }) => {
3070                match (prefer_spark, spark_transfer_fee_sats) {
3071                    (true, Some(fee)) => fee,
3072                    _ => lightning_fee_sats,
3073                }
3074            }
3075            _ => lightning_fee_sats,
3076        };
3077        // The absolute minimum amount out is the lightning invoice amount plus fee
3078        let min_amount_out = request
3079            .prepare_response
3080            .amount
3081            .saturating_add(u128::from(fee_sats));
3082
3083        self.convert_token(
3084            conversion_options,
3085            conversion_purpose,
3086            request.prepare_response.token_identifier.as_ref(),
3087            min_amount_out,
3088        )
3089        .await
3090    }
3091
3092    async fn convert_token_for_bitcoin_address(
3093        &self,
3094        conversion_options: &ConversionOptions,
3095        fee_quote: &SendOnchainFeeQuote,
3096        request: &SendPaymentRequest,
3097        conversion_purpose: &ConversionPurpose,
3098    ) -> Result<TokenConversionResponse, SdkError> {
3099        // Determine the fee to be used based on confirmation speed
3100        let fee_sats = if let Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) =
3101            &request.options
3102        {
3103            match confirmation_speed {
3104                OnchainConfirmationSpeed::Slow => fee_quote.speed_slow.total_fee_sat(),
3105                OnchainConfirmationSpeed::Medium => fee_quote.speed_medium.total_fee_sat(),
3106                OnchainConfirmationSpeed::Fast => fee_quote.speed_fast.total_fee_sat(),
3107            }
3108        } else {
3109            fee_quote.speed_fast.total_fee_sat()
3110        };
3111        // The absolute minimum amount out is the amount plus fee
3112        let min_amount_out = request
3113            .prepare_response
3114            .amount
3115            .saturating_add(u128::from(fee_sats));
3116
3117        self.convert_token(
3118            conversion_options,
3119            conversion_purpose,
3120            request.prepare_response.token_identifier.as_ref(),
3121            min_amount_out,
3122        )
3123        .await
3124    }
3125
3126    #[allow(clippy::too_many_lines)]
3127    async fn convert_token(
3128        &self,
3129        conversion_options: &ConversionOptions,
3130        conversion_purpose: &ConversionPurpose,
3131        token_identifier: Option<&String>,
3132        min_amount_out: u128,
3133    ) -> Result<TokenConversionResponse, SdkError> {
3134        let conversion_pool = self
3135            .get_conversion_pool(conversion_options, token_identifier, min_amount_out)
3136            .await?;
3137        let conversion_estimate = self
3138            .estimate_conversion_internal(&conversion_pool, conversion_options, min_amount_out)
3139            .await?
3140            .ok_or(SdkError::Generic(
3141                "No conversion estimate available".to_string(),
3142            ))?;
3143        // Execute the conversion
3144        let pool_id = conversion_pool.pool.lp_public_key;
3145        let response_res = self
3146            .flashnet_client
3147            .execute_swap(ExecuteSwapRequest {
3148                asset_in_address: conversion_pool.asset_in_address.clone(),
3149                asset_out_address: conversion_pool.asset_out_address.clone(),
3150                pool_id,
3151                amount_in: conversion_estimate.amount,
3152                max_slippage_bps: conversion_options
3153                    .max_slippage_bps
3154                    .unwrap_or(DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS),
3155                min_amount_out,
3156                integrator_fee_rate_bps: None,
3157                integrator_public_key: None,
3158            })
3159            .await;
3160        match response_res {
3161            Ok(response) => {
3162                info!(
3163                    "Conversion executed: accepted {}, error {:?}",
3164                    response.accepted, response.error
3165                );
3166                let (sent_payment_id, received_payment_id) = self
3167                    .update_payment_conversion_info(
3168                        &pool_id,
3169                        response.transfer_id,
3170                        response.outbound_transfer_id,
3171                        response.refund_transfer_id,
3172                        response.fee_amount,
3173                        conversion_purpose,
3174                    )
3175                    .await?;
3176                if let Some(received_payment_id) = received_payment_id
3177                    && response.accepted
3178                {
3179                    Ok(TokenConversionResponse {
3180                        sent_payment_id,
3181                        received_payment_id,
3182                    })
3183                } else {
3184                    let error_message = response
3185                        .error
3186                        .unwrap_or("Conversion not accepted".to_string());
3187                    Err(SdkError::Generic(format!(
3188                        "Convert token failed, refund in progress: {error_message}",
3189                    )))
3190                }
3191            }
3192            Err(e) => {
3193                error!("Convert token failed: {e:?}");
3194                if let FlashnetError::Execution {
3195                    transaction_identifier: Some(transaction_identifier),
3196                    source,
3197                } = &e
3198                {
3199                    let _ = self
3200                        .update_payment_conversion_info(
3201                            &pool_id,
3202                            transaction_identifier.clone(),
3203                            None,
3204                            None,
3205                            None,
3206                            conversion_purpose,
3207                        )
3208                        .await;
3209                    let _ = self.conversion_refund_trigger.send(());
3210                    Err(SdkError::Generic(format!(
3211                        "Convert token failed, refund pending: {}",
3212                        *source.clone()
3213                    )))
3214                } else {
3215                    Err(e.into())
3216                }
3217            }
3218        }
3219    }
3220
3221    async fn get_conversion_pool(
3222        &self,
3223        conversion_options: &ConversionOptions,
3224        token_identifier: Option<&String>,
3225        amount_out: u128,
3226    ) -> Result<TokenConversionPool, SdkError> {
3227        let conversion_type = &conversion_options.conversion_type;
3228        let (asset_in_address, asset_out_address) =
3229            conversion_type.as_asset_addresses(token_identifier)?;
3230
3231        // List available pools for the asset pair
3232        let a_in_pools_fut = self.flashnet_client.list_pools(ListPoolsRequest {
3233            asset_a_address: Some(asset_in_address.clone()),
3234            asset_b_address: Some(asset_out_address.clone()),
3235            sort: Some(PoolSortOrder::Volume24hDesc),
3236            ..Default::default()
3237        });
3238        let b_in_pools_fut = self.flashnet_client.list_pools(ListPoolsRequest {
3239            asset_a_address: Some(asset_out_address.clone()),
3240            asset_b_address: Some(asset_in_address.clone()),
3241            sort: Some(PoolSortOrder::Volume24hDesc),
3242            ..Default::default()
3243        });
3244        let (a_in_pools_res, b_in_pools_res) = tokio::join!(a_in_pools_fut, b_in_pools_fut);
3245        let mut pools = a_in_pools_res.map_or(HashMap::new(), |res| {
3246            res.pools
3247                .into_iter()
3248                .map(|pool| (pool.lp_public_key, pool))
3249                .collect::<HashMap<_, _>>()
3250        });
3251        if let Ok(res) = b_in_pools_res {
3252            pools.extend(res.pools.into_iter().map(|pool| (pool.lp_public_key, pool)));
3253        }
3254        let pools = pools.into_values().collect::<Vec<_>>();
3255        if pools.is_empty() {
3256            warn!(
3257                "No conversion pools available: in address {asset_in_address}, out address {asset_out_address}",
3258            );
3259            return Err(SdkError::Generic(
3260                "No conversion pools available".to_string(),
3261            ));
3262        }
3263
3264        // Extract max_slippage_bps with default fallback
3265        let max_slippage_bps = conversion_options
3266            .max_slippage_bps
3267            .unwrap_or(DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS);
3268
3269        // Select the best pool using multi-factor scoring
3270        let pool = flashnet::select_best_pool(
3271            &pools,
3272            &asset_in_address,
3273            amount_out,
3274            max_slippage_bps,
3275            self.config.network.into(),
3276        )?;
3277
3278        Ok(TokenConversionPool {
3279            asset_in_address,
3280            asset_out_address,
3281            pool,
3282        })
3283    }
3284
3285    async fn estimate_conversion(
3286        &self,
3287        conversion_options: Option<&ConversionOptions>,
3288        token_identifier: Option<&String>,
3289        amount_out: u128,
3290    ) -> Result<Option<ConversionEstimate>, SdkError> {
3291        let Some(conversion_options) = conversion_options else {
3292            return Ok(None);
3293        };
3294        let conversion_pool = self
3295            .get_conversion_pool(conversion_options, token_identifier, amount_out)
3296            .await?;
3297
3298        self.estimate_conversion_internal(&conversion_pool, conversion_options, amount_out)
3299            .await
3300    }
3301
3302    async fn estimate_conversion_internal(
3303        &self,
3304        conversion_pool: &TokenConversionPool,
3305        conversion_options: &ConversionOptions,
3306        amount_out: u128,
3307    ) -> Result<Option<ConversionEstimate>, SdkError> {
3308        let TokenConversionPool {
3309            asset_in_address,
3310            asset_out_address,
3311            pool,
3312        } = conversion_pool;
3313        // Calculate the required amount in for the desired amount out
3314        let amount_in = pool.calculate_amount_in(
3315            asset_in_address,
3316            amount_out,
3317            conversion_options
3318                .max_slippage_bps
3319                .unwrap_or(DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS),
3320            self.config.network.into(),
3321        )?;
3322        // Simulate the swap to validate the conversion
3323        let response = self
3324            .flashnet_client
3325            .simulate_swap(SimulateSwapRequest {
3326                asset_in_address: asset_in_address.clone(),
3327                asset_out_address: asset_out_address.clone(),
3328                pool_id: pool.lp_public_key,
3329                amount_in,
3330                integrator_bps: None,
3331            })
3332            .await?;
3333        if response.amount_out < amount_out {
3334            return Err(SdkError::Generic(format!(
3335                "Validation returned {} but expected at least {amount_out}",
3336                response.amount_out
3337            )));
3338        }
3339        Ok(response.fee_paid_asset_in.map(|fee| ConversionEstimate {
3340            options: conversion_options.clone(),
3341            amount: amount_in,
3342            fee,
3343        }))
3344    }
3345
3346    /// Fetches a payment by its conversion identifier.
3347    /// The identifier can be either a spark transfer id or a token transaction hash.
3348    async fn fetch_payment_by_conversion_identifier(
3349        &self,
3350        identifier: &str,
3351        tx_inputs_are_ours: bool,
3352    ) -> Result<Payment, SdkError> {
3353        debug!("Fetching conversion payment for identifier: {}", identifier);
3354        let payment = if let Ok(transfer_id) = TransferId::from_str(identifier) {
3355            let transfers = self
3356                .spark_wallet
3357                .list_transfers(ListTransfersRequest {
3358                    transfer_ids: vec![transfer_id],
3359                    ..Default::default()
3360                })
3361                .await?;
3362            let transfer = transfers
3363                .items
3364                .first()
3365                .cloned()
3366                .ok_or_else(|| SdkError::Generic("Transfer not found".to_string()))?;
3367            transfer.try_into()
3368        } else {
3369            let token_transactions = self
3370                .spark_wallet
3371                .list_token_transactions(ListTokenTransactionsRequest {
3372                    token_transaction_hashes: vec![identifier.to_string()],
3373                    ..Default::default()
3374                })
3375                .await?;
3376            let token_transaction = token_transactions
3377                .items
3378                .first()
3379                .ok_or_else(|| SdkError::Generic("Token transaction not found".to_string()))?;
3380            let object_repository = ObjectCacheRepository::new(self.storage.clone());
3381            let payments = token_transaction_to_payments(
3382                &self.spark_wallet,
3383                &object_repository,
3384                token_transaction,
3385                tx_inputs_are_ours,
3386            )
3387            .await?;
3388            payments.first().cloned().ok_or_else(|| {
3389                SdkError::Generic("Payment not found for token transaction".to_string())
3390            })
3391        };
3392        payment
3393            .inspect(|p| debug!("Found payment: {p:?}"))
3394            .inspect_err(|e| debug!("No payment found: {e}"))
3395    }
3396
3397    /// Updates the payment with the conversion info.
3398    ///
3399    /// Arguments:
3400    /// * `pool_id` - The pool id used for the conversion.
3401    /// * `outbound_identifier` - The outbound spark transfer id or token transaction hash.
3402    /// * `inbound_identifier` - The inbound spark transfer id or token transaction hash if the conversion was successful.
3403    /// * `refund_identifier` - The inbound refund spark transfer id or token transaction hash if the conversion was refunded.
3404    /// * `fee` - The fee paid for the conversion.
3405    ///
3406    /// Returns:
3407    /// * The sent payment id of the conversion.
3408    /// * The received payment id of the conversion.
3409    async fn update_payment_conversion_info(
3410        &self,
3411        pool_id: &PublicKey,
3412        outbound_identifier: String,
3413        inbound_identifier: Option<String>,
3414        refund_identifier: Option<String>,
3415        fee: Option<u128>,
3416        purpose: &ConversionPurpose,
3417    ) -> Result<(String, Option<String>), SdkError> {
3418        debug!(
3419            "Updating payment conversion info for pool_id: {pool_id}, outbound_identifier: {outbound_identifier}, inbound_identifier: {inbound_identifier:?}, refund_identifier: {refund_identifier:?}"
3420        );
3421        let cache = ObjectCacheRepository::new(self.storage.clone());
3422        let status = match (&inbound_identifier, &refund_identifier) {
3423            (Some(_), _) => ConversionStatus::Completed,
3424            (None, Some(_)) => ConversionStatus::Refunded,
3425            _ => ConversionStatus::RefundNeeded,
3426        };
3427        let pool_id_str = pool_id.to_string();
3428        let conversion_id = uuid::Uuid::now_v7().to_string();
3429
3430        // Update the sent payment metadata
3431        let sent_payment = self
3432            .fetch_payment_by_conversion_identifier(&outbound_identifier, true)
3433            .await?;
3434        let sent_payment_id = sent_payment.id.clone();
3435        self.storage
3436            .set_payment_metadata(
3437                sent_payment_id.clone(),
3438                PaymentMetadata {
3439                    conversion_info: Some(ConversionInfo {
3440                        pool_id: pool_id_str.clone(),
3441                        conversion_id: conversion_id.clone(),
3442                        status: status.clone(),
3443                        fee,
3444                        purpose: None,
3445                    }),
3446                    ..Default::default()
3447                },
3448            )
3449            .await?;
3450
3451        // Update the received payment metadata if available
3452        let received_payment_id = if let Some(identifier) = &inbound_identifier {
3453            let metadata = PaymentMetadata {
3454                conversion_info: Some(ConversionInfo {
3455                    pool_id: pool_id_str.clone(),
3456                    conversion_id: conversion_id.clone(),
3457                    status: status.clone(),
3458                    fee: None,
3459                    purpose: Some(purpose.clone()),
3460                }),
3461                ..Default::default()
3462            };
3463            if let Ok(payment) = self
3464                .fetch_payment_by_conversion_identifier(identifier, false)
3465                .await
3466            {
3467                self.storage
3468                    .set_payment_metadata(payment.id.clone(), metadata)
3469                    .await?;
3470                Some(payment.id)
3471            } else {
3472                cache.save_payment_metadata(identifier, &metadata).await?;
3473                Some(identifier.clone())
3474            }
3475        } else {
3476            None
3477        };
3478
3479        // Update the refund payment metadata if available
3480        if let Some(identifier) = &refund_identifier {
3481            let metadata = PaymentMetadata {
3482                conversion_info: Some(ConversionInfo {
3483                    pool_id: pool_id_str,
3484                    conversion_id,
3485                    status,
3486                    fee: None,
3487                    purpose: None,
3488                }),
3489                ..Default::default()
3490            };
3491            if let Ok(payment) = self
3492                .fetch_payment_by_conversion_identifier(identifier, false)
3493                .await
3494            {
3495                self.storage
3496                    .set_payment_metadata(payment.id.clone(), metadata)
3497                    .await?;
3498            } else {
3499                cache.save_payment_metadata(identifier, &metadata).await?;
3500            }
3501        }
3502
3503        self.storage.insert_payment(sent_payment).await?;
3504
3505        Ok((sent_payment_id, received_payment_id))
3506    }
3507}
3508
3509fn is_payment_match(payment: &Payment, identifier: &WaitForPaymentIdentifier) -> bool {
3510    match identifier {
3511        WaitForPaymentIdentifier::PaymentId(payment_id) => payment.id == *payment_id,
3512        WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
3513            if let Some(details) = &payment.details {
3514                match details {
3515                    PaymentDetails::Lightning { invoice, .. } => {
3516                        invoice.to_lowercase() == payment_request.to_lowercase()
3517                    }
3518                    PaymentDetails::Spark {
3519                        invoice_details: invoice,
3520                        ..
3521                    }
3522                    | PaymentDetails::Token {
3523                        invoice_details: invoice,
3524                        ..
3525                    } => {
3526                        if let Some(invoice) = invoice {
3527                            invoice.invoice.to_lowercase() == payment_request.to_lowercase()
3528                        } else {
3529                            false
3530                        }
3531                    }
3532                    PaymentDetails::Withdraw { tx_id: _ }
3533                    | PaymentDetails::Deposit { tx_id: _ } => false,
3534                }
3535            } else {
3536                false
3537            }
3538        }
3539    }
3540}
3541
3542struct BalanceWatcher {
3543    spark_wallet: Arc<SparkWallet>,
3544    storage: Arc<dyn Storage>,
3545}
3546
3547impl BalanceWatcher {
3548    fn new(spark_wallet: Arc<SparkWallet>, storage: Arc<dyn Storage>) -> Self {
3549        Self {
3550            spark_wallet,
3551            storage,
3552        }
3553    }
3554}
3555
3556#[macros::async_trait]
3557impl EventListener for BalanceWatcher {
3558    async fn on_event(&self, event: SdkEvent) {
3559        match event {
3560            SdkEvent::PaymentSucceeded { .. } | SdkEvent::ClaimedDeposits { .. } => {
3561                match update_balances(self.spark_wallet.clone(), self.storage.clone()).await {
3562                    Ok(()) => info!("Balance updated successfully"),
3563                    Err(e) => error!("Failed to update balance: {e:?}"),
3564                }
3565            }
3566            _ => {}
3567        }
3568    }
3569}
3570
3571async fn update_balances(
3572    spark_wallet: Arc<SparkWallet>,
3573    storage: Arc<dyn Storage>,
3574) -> Result<(), SdkError> {
3575    let balance_sats = spark_wallet.get_balance().await?;
3576    let token_balances = spark_wallet
3577        .get_token_balances()
3578        .await?
3579        .into_iter()
3580        .map(|(k, v)| (k, v.into()))
3581        .collect();
3582    let object_repository = ObjectCacheRepository::new(storage.clone());
3583
3584    object_repository
3585        .save_account_info(&CachedAccountInfo {
3586            balance_sats,
3587            token_balances,
3588        })
3589        .await?;
3590    let identity_public_key = spark_wallet.get_identity_public_key();
3591    info!(
3592        "Balance updated successfully {} for identity {}",
3593        balance_sats, identity_public_key
3594    );
3595    Ok(())
3596}
3597
3598struct InternalEventListener {
3599    tx: mpsc::Sender<SdkEvent>,
3600}
3601
3602impl InternalEventListener {
3603    #[allow(unused)]
3604    pub fn new(tx: mpsc::Sender<SdkEvent>) -> Self {
3605        Self { tx }
3606    }
3607}
3608
3609#[macros::async_trait]
3610impl EventListener for InternalEventListener {
3611    async fn on_event(&self, event: SdkEvent) {
3612        let _ = self.tx.send(event).await;
3613    }
3614}
3615
3616fn process_success_action(
3617    payment: &Payment,
3618    success_action: Option<&SuccessAction>,
3619) -> Result<Option<SuccessActionProcessed>, LnurlError> {
3620    let Some(success_action) = success_action else {
3621        return Ok(None);
3622    };
3623
3624    let data = match success_action {
3625        SuccessAction::Aes { data } => data,
3626        SuccessAction::Message { data } => {
3627            return Ok(Some(SuccessActionProcessed::Message { data: data.clone() }));
3628        }
3629        SuccessAction::Url { data } => {
3630            return Ok(Some(SuccessActionProcessed::Url { data: data.clone() }));
3631        }
3632    };
3633
3634    let Some(PaymentDetails::Lightning { preimage, .. }) = &payment.details else {
3635        return Err(LnurlError::general(format!(
3636            "Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.",
3637            payment.details
3638        )));
3639    };
3640
3641    let Some(preimage) = preimage else {
3642        return Ok(None);
3643    };
3644
3645    let preimage =
3646        sha256::Hash::from_str(preimage).map_err(|_| LnurlError::general("Invalid preimage"))?;
3647    let preimage = preimage.as_byte_array();
3648    let result: AesSuccessActionDataResult = match (data, preimage).try_into() {
3649        Ok(data) => AesSuccessActionDataResult::Decrypted { data },
3650        Err(e) => AesSuccessActionDataResult::ErrorStatus {
3651            reason: e.to_string(),
3652        },
3653    };
3654
3655    Ok(Some(SuccessActionProcessed::Aes { result }))
3656}
3657
3658fn validate_breez_api_key(api_key: &str) -> Result<(), SdkError> {
3659    let api_key_decoded = base64::engine::general_purpose::STANDARD
3660        .decode(api_key.as_bytes())
3661        .map_err(|err| {
3662            SdkError::Generic(format!(
3663                "Could not base64 decode the Breez API key: {err:?}"
3664            ))
3665        })?;
3666    let (_rem, cert) = parse_x509_certificate(&api_key_decoded).map_err(|err| {
3667        SdkError::Generic(format!("Invalid certificate for Breez API key: {err:?}"))
3668    })?;
3669
3670    let issuer = cert
3671        .issuer()
3672        .iter_common_name()
3673        .next()
3674        .and_then(|cn| cn.as_str().ok());
3675    match issuer {
3676        Some(common_name) => {
3677            if !common_name.starts_with("Breez") {
3678                return Err(SdkError::Generic(
3679                    "Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted"
3680                        .to_string()
3681                ));
3682            }
3683        }
3684        _ => {
3685            return Err(SdkError::Generic(
3686                "Could not parse Breez API key certificate: issuer is invalid or not found."
3687                    .to_string(),
3688            ));
3689        }
3690    }
3691
3692    Ok(())
3693}