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