breez_sdk_spark/
sdk.rs

1use base64::Engine;
2use bitcoin::{
3    consensus::serialize,
4    hashes::{Hash, sha256},
5    hex::DisplayHex,
6};
7use breez_sdk_common::input::InputType;
8pub use breez_sdk_common::input::parse as parse_input;
9use breez_sdk_common::{
10    lnurl::{
11        error::LnurlError,
12        pay::{
13            AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed,
14            ValidatedCallbackResponse, validate_lnurl_pay,
15        },
16    },
17    rest::RestClient,
18};
19use spark_wallet::{
20    DefaultSigner, ExitSpeed, Order, PagingFilter, PayLightningInvoiceResult, SparkAddress,
21    SparkWallet, WalletEvent, WalletTransfer,
22};
23use std::{str::FromStr, sync::Arc};
24use tracing::{error, info, trace};
25use web_time::{Duration, SystemTime};
26
27use tokio::{select, sync::watch};
28use tokio_with_wasm::alias as tokio;
29use web_time::Instant;
30use x509_parser::parse_x509_certificate;
31
32use crate::{
33    BitcoinChainService, ClaimDepositRequest, ClaimDepositResponse, DepositInfo, Fee,
34    GetPaymentRequest, GetPaymentResponse, ListUnclaimedDepositsRequest,
35    ListUnclaimedDepositsResponse, LnurlPayInfo, LnurlPayRequest, LnurlPayResponse, Logger,
36    Network, PaymentDetails, PaymentStatus, PrepareLnurlPayRequest, PrepareLnurlPayResponse,
37    RefundDepositRequest, RefundDepositResponse, SendPaymentOptions,
38    error::SdkError,
39    events::{EventEmitter, EventListener, SdkEvent},
40    logger,
41    models::{
42        Config, GetInfoRequest, GetInfoResponse, ListPaymentsRequest, ListPaymentsResponse,
43        Payment, PrepareSendPaymentRequest, PrepareSendPaymentResponse, ReceivePaymentMethod,
44        ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentMethod, SendPaymentRequest,
45        SendPaymentResponse, SyncWalletRequest, SyncWalletResponse,
46    },
47    persist::{
48        CachedAccountInfo, CachedSyncInfo, ObjectCacheRepository, PaymentMetadata,
49        StaticDepositAddress, Storage, UpdateDepositPayload,
50    },
51    utils::{
52        deposit_chain_syncer::DepositChainSyncer,
53        utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo},
54    },
55};
56
57#[derive(Clone, Debug)]
58enum SyncType {
59    Full,
60    PaymentsOnly,
61}
62
63/// `BreezSDK` is a wrapper around `SparkSDK` that provides a more structured API
64/// with request/response objects and comprehensive error handling.
65#[derive(Clone)]
66#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
67pub struct BreezSdk {
68    config: Config,
69    spark_wallet: Arc<SparkWallet<DefaultSigner>>,
70    storage: Arc<dyn Storage>,
71    chain_service: Arc<dyn BitcoinChainService>,
72    lnurl_client: Arc<dyn RestClient>,
73    event_emitter: Arc<EventEmitter>,
74    shutdown_sender: watch::Sender<()>,
75    shutdown_receiver: watch::Receiver<()>,
76    sync_trigger: tokio::sync::broadcast::Sender<SyncType>,
77}
78
79#[cfg_attr(feature = "uniffi", uniffi::export)]
80pub fn init_logging(
81    log_dir: Option<String>,
82    app_logger: Option<Box<dyn Logger>>,
83    log_filter: Option<String>,
84) -> Result<(), SdkError> {
85    logger::init_logging(log_dir, app_logger, log_filter)
86}
87
88#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
89#[cfg_attr(feature = "uniffi", uniffi::export)]
90#[allow(clippy::needless_pass_by_value)]
91pub fn default_storage(data_dir: String) -> Result<Arc<dyn Storage>, SdkError> {
92    let db_path = std::path::PathBuf::from_str(&data_dir)?;
93
94    let storage = crate::SqliteStorage::new(&db_path)?;
95    Ok(Arc::new(storage))
96}
97
98#[cfg_attr(feature = "uniffi", uniffi::export)]
99pub fn default_config(network: Network) -> Config {
100    Config {
101        api_key: None,
102        network,
103        sync_interval_secs: 60, // every 1 minute
104        max_deposit_claim_fee: None,
105    }
106}
107
108#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
109pub async fn parse(input: &str) -> Result<InputType, SdkError> {
110    Ok(parse_input(input).await?)
111}
112
113impl BreezSdk {
114    /// Creates a new instance of the `BreezSdk`
115    ///
116    /// # Arguments
117    ///
118    /// * `config` - The Sdk configuration object
119    /// * `signer` - Implementation of the `SparkSigner` trait
120    /// * `storage` - Optional storage implementation for persistent data
121    /// * `chain_service` - Implementation of the `ChainService` trait
122    /// * `shutdown_sender` - Sender for shutdown signal
123    /// * `shutdown_receiver` - Receiver for shutdown signal
124    ///
125    /// # Returns
126    ///
127    /// Result containing either the initialized `BreezSdk` or an `SdkError`
128    pub(crate) async fn new(
129        config: Config,
130        signer: DefaultSigner,
131        storage: Arc<dyn Storage>,
132        chain_service: Arc<dyn BitcoinChainService>,
133        lnurl_client: Arc<dyn RestClient>,
134        shutdown_sender: watch::Sender<()>,
135        shutdown_receiver: watch::Receiver<()>,
136    ) -> Result<Self, SdkError> {
137        let spark_wallet_config =
138            spark_wallet::SparkWalletConfig::default_config(config.clone().network.into());
139        let spark_wallet = SparkWallet::connect(spark_wallet_config, signer).await?;
140
141        match &config.api_key {
142            Some(api_key) => validate_breez_api_key(api_key)?,
143            None => return Err(SdkError::Generic("Missing Breez API key".to_string())),
144        }
145        let sdk = Self {
146            config,
147            spark_wallet: Arc::new(spark_wallet),
148            storage,
149            chain_service,
150            lnurl_client,
151            event_emitter: Arc::new(EventEmitter::new()),
152            shutdown_sender,
153            shutdown_receiver,
154            sync_trigger: tokio::sync::broadcast::channel(10).0,
155        };
156        Ok(sdk)
157    }
158
159    /// Connects to the Spark network using the provided configuration and mnemonic.
160    ///
161    /// # Arguments
162    ///
163    /// * `request` - The connection request object
164    ///
165    /// # Returns
166    ///
167    /// Result containing either the initialized `BreezSdk` or an `SdkError`
168    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
169    pub async fn connect(request: crate::ConnectRequest) -> Result<Self, SdkError> {
170        let db_path = std::path::PathBuf::from_str(&request.storage_dir)?;
171        let path_suffix: String = sha256::Hash::hash(request.mnemonic.as_bytes())
172            .to_string()
173            .chars()
174            .take(8)
175            .collect();
176        let storage_dir = db_path
177            .join(request.config.network.to_string().to_lowercase())
178            .join(path_suffix);
179
180        let storage = default_storage(storage_dir.to_string_lossy().to_string())?;
181        let builder = crate::SdkBuilder::new(request.config, request.mnemonic, storage);
182        builder.build().await
183    }
184
185    /// Starts the SDK's background tasks
186    ///
187    /// This method initiates the following backround tasks:
188    /// 1. `periodic_sync`: the wallet with the Spark network    
189    /// 2. `monitor_deposits`: monitors for new deposits
190    pub(crate) fn start(&self) {
191        self.periodic_sync();
192    }
193
194    fn periodic_sync(&self) {
195        let sdk = self.clone();
196        let mut shutdown_receiver = sdk.shutdown_receiver.clone();
197        let mut subscription = sdk.spark_wallet.subscribe_events();
198        let sync_trigger_sender = sdk.sync_trigger.clone();
199        let mut sync_trigger_receiver = sdk.sync_trigger.clone().subscribe();
200        let mut last_sync_time = SystemTime::now();
201        let sync_interval = u64::from(self.config.sync_interval_secs);
202        tokio::spawn(async move {
203            loop {
204                tokio::select! {
205                    _ = shutdown_receiver.changed() => {
206                        info!("Deposit tracking loop shutdown signal received");
207                        return;
208                    }
209                    event = subscription.recv() => {
210                        match event {
211                            Ok(event) => {
212                                info!("Received event: {event}");
213                                trace!("Received event: {:?}", event);
214                                sdk.handle_wallet_event(event);
215                            }
216                            Err(e) => {
217                                error!("Failed to receive event: {e:?}");
218                            }
219                        }
220                    }
221                    () = async {
222                      let sync_type_res = sync_trigger_receiver.recv().await;
223                      if let Ok(sync_type) = sync_type_res   {
224                          info!("Sync trigger changed: {:?}", &sync_type);
225
226                          if let Err(e) = sdk.sync_wallet_internal(sync_type.clone()).await {
227                              error!("Failed to sync wallet: {e:?}");
228                          } else if matches!(sync_type, SyncType::Full) {
229                            last_sync_time = SystemTime::now();
230                          }
231                      }
232                  } => {}
233                    // Ensure we sync at least the configured interval
234                    () = tokio::time::sleep(Duration::from_secs(10)) => {
235                        let now = SystemTime::now();
236                        if let Ok(elapsed) = now.duration_since(last_sync_time) && elapsed.as_secs() >= sync_interval
237                            && let Err(e) = sync_trigger_sender.send(SyncType::Full) {
238                            error!("Failed to trigger periodic sync: {e:?}");
239                        }
240                    }
241                }
242            }
243        });
244    }
245
246    fn handle_wallet_event(&self, event: WalletEvent) {
247        match event {
248            WalletEvent::DepositConfirmed(_) => {
249                info!("Deposit confirmed");
250            }
251            WalletEvent::StreamConnected => {
252                info!("Stream connected");
253            }
254            WalletEvent::StreamDisconnected => {
255                info!("Stream disconnected");
256            }
257            WalletEvent::Synced => {
258                info!("Synced");
259                if let Err(e) = self.sync_trigger.send(SyncType::Full) {
260                    error!("Failed to sync wallet: {e:?}");
261                }
262            }
263            WalletEvent::TransferClaimed(transfer) => {
264                info!("Transfer claimed");
265                if let Ok(payment) = transfer.try_into() {
266                    self.event_emitter
267                        .emit(&SdkEvent::PaymentSucceeded { payment });
268                }
269                if let Err(e) = self.sync_trigger.send(SyncType::PaymentsOnly) {
270                    error!("Failed to sync wallet: {e:?}");
271                }
272            }
273        }
274    }
275
276    async fn sync_wallet_internal(&self, sync_type: SyncType) -> Result<(), SdkError> {
277        let start_time = Instant::now();
278        if let SyncType::Full = sync_type {
279            // Sync with the Spark network
280            info!("sync_wallet_internal: Syncing with Spark network");
281            self.spark_wallet.sync().await?;
282            info!("sync_wallet_internal: Synced with Spark network completed");
283        }
284        self.sync_payments_to_storage().await?;
285        info!("sync_wallet_internal: Synced payments to storage completed");
286        self.check_and_claim_static_deposits().await?;
287        info!("sync_wallet_internal: Checked and claimed static deposits completed");
288        let elapsed = start_time.elapsed();
289        info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}");
290        self.event_emitter.emit(&SdkEvent::Synced {});
291        Ok(())
292    }
293
294    /// Synchronizes payments from transfers to persistent storage
295    async fn sync_payments_to_storage(&self) -> Result<(), SdkError> {
296        const BATCH_SIZE: u64 = 50;
297
298        // Sync balance
299        let balance = self.spark_wallet.get_balance().await?;
300        let object_repository = ObjectCacheRepository::new(self.storage.clone());
301        object_repository
302            .save_account_info(&CachedAccountInfo {
303                balance_sats: balance,
304            })
305            .await?;
306
307        // Get the last offset we processed from storage
308        let cached_sync_info = object_repository
309            .fetch_sync_info()
310            .await?
311            .unwrap_or_default();
312        let current_offset = cached_sync_info.offset;
313
314        // We'll keep querying in batches until we have all transfers
315        let mut next_offset = current_offset;
316        let mut has_more = true;
317        info!("Syncing payments to storage, offset = {next_offset}");
318        let mut pending_payments: u64 = 0;
319        while has_more {
320            // Get batch of transfers starting from current offset
321            let transfers_response = self
322                .spark_wallet
323                .list_transfers(Some(PagingFilter::new(
324                    Some(next_offset),
325                    Some(BATCH_SIZE),
326                    Some(Order::Ascending),
327                )))
328                .await?;
329
330            info!(
331                "Syncing payments to storage, offset = {next_offset}, transfers = {}",
332                transfers_response.len()
333            );
334            // Process transfers in this batch
335            for transfer in &transfers_response {
336                // Create a payment record
337                let payment: Payment = transfer.clone().try_into()?;
338                // Insert payment into storage
339                if let Err(err) = self.storage.insert_payment(payment.clone()).await {
340                    error!("Failed to insert payment: {err:?}");
341                }
342                if payment.status == PaymentStatus::Pending {
343                    pending_payments = pending_payments.saturating_add(1);
344                }
345                info!("Inserted payment: {payment:?}");
346            }
347
348            // Check if we have more transfers to fetch
349            next_offset = next_offset.saturating_add(u64::try_from(transfers_response.len())?);
350            // Update our last processed offset in the storage. We should remove pending payments
351            // from the offset as they might be removed from the list later.
352            let save_res = object_repository
353                .save_sync_info(&CachedSyncInfo {
354                    offset: next_offset.saturating_sub(pending_payments),
355                })
356                .await;
357
358            if let Err(err) = save_res {
359                error!("Failed to update last sync offset: {err:?}");
360            }
361            has_more = transfers_response.len() as u64 == BATCH_SIZE;
362        }
363
364        Ok(())
365    }
366
367    async fn check_and_claim_static_deposits(&self) -> Result<(), SdkError> {
368        let to_claim = DepositChainSyncer::new(
369            self.chain_service.clone(),
370            self.storage.clone(),
371            self.spark_wallet.clone(),
372        )
373        .sync()
374        .await?;
375
376        let mut claimed_deposits: Vec<DepositInfo> = Vec::new();
377        let mut unclaimed_deposits: Vec<DepositInfo> = Vec::new();
378        for detailed_utxo in to_claim {
379            match self
380                .claim_utxo(&detailed_utxo, self.config.max_deposit_claim_fee.clone())
381                .await
382            {
383                Ok(_) => {
384                    info!("Claimed utxo {}:{}", detailed_utxo.txid, detailed_utxo.vout);
385                    self.storage
386                        .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
387                        .await?;
388                    claimed_deposits.push(detailed_utxo.into());
389                }
390                Err(e) => {
391                    error!(
392                        "Failed to claim utxo {}:{}: {e}",
393                        detailed_utxo.txid, detailed_utxo.vout
394                    );
395                    self.storage
396                        .update_deposit(
397                            detailed_utxo.txid.to_string(),
398                            detailed_utxo.vout,
399                            UpdateDepositPayload::ClaimError {
400                                error: e.clone().into(),
401                            },
402                        )
403                        .await?;
404                    let mut unclaimed_deposit: DepositInfo = detailed_utxo.clone().into();
405                    unclaimed_deposit.claim_error = Some(e.into());
406                    unclaimed_deposits.push(unclaimed_deposit);
407                }
408            }
409        }
410
411        info!("background claim completed, unclaimed deposits: {unclaimed_deposits:?}");
412
413        if !unclaimed_deposits.is_empty() {
414            self.event_emitter
415                .emit(&SdkEvent::ClaimDepositsFailed { unclaimed_deposits });
416        }
417        if !claimed_deposits.is_empty() {
418            self.event_emitter
419                .emit(&SdkEvent::ClaimDepositsSucceeded { claimed_deposits });
420        }
421        Ok(())
422    }
423
424    async fn claim_utxo(
425        &self,
426        detailed_utxo: &DetailedUtxo,
427        max_claim_fee: Option<Fee>,
428    ) -> Result<WalletTransfer, SdkError> {
429        info!(
430            "Fetching static deposit claim quote for deposit tx {}:{} and amount: {}",
431            detailed_utxo.txid, detailed_utxo.vout, detailed_utxo.value
432        );
433
434        let quote = self
435            .spark_wallet
436            .fetch_static_deposit_claim_quote(detailed_utxo.tx.clone(), Some(detailed_utxo.vout))
437            .await?;
438        let spark_requested_fee = detailed_utxo.value.saturating_sub(quote.credit_amount_sats);
439        if let Some(max_deposit_claim_fee) = max_claim_fee {
440            match max_deposit_claim_fee {
441                Fee::Fixed { amount } => {
442                    info!(
443                        "User max fee: {} spark requested fee: {}",
444                        amount, spark_requested_fee
445                    );
446                    if spark_requested_fee > amount {
447                        return Err(SdkError::DepositClaimFeeExceeded {
448                            tx: detailed_utxo.txid.to_string(),
449                            vout: detailed_utxo.vout,
450                            max_fee: max_deposit_claim_fee,
451                            actual_fee: spark_requested_fee,
452                        });
453                    }
454                }
455                Fee::Rate { sat_per_vbyte } => {
456                    // The claim tx size is 99 vbytes
457                    const CLAIM_TX_SIZE: u64 = 99;
458                    let user_max_fee = CLAIM_TX_SIZE.saturating_mul(sat_per_vbyte);
459                    info!(
460                        "User max fee: {} spark requested fee: {}",
461                        user_max_fee, spark_requested_fee
462                    );
463                    if spark_requested_fee > user_max_fee {
464                        return Err(SdkError::DepositClaimFeeExceeded {
465                            tx: detailed_utxo.txid.to_string(),
466                            vout: detailed_utxo.vout,
467                            max_fee: max_deposit_claim_fee,
468                            actual_fee: spark_requested_fee,
469                        });
470                    }
471                }
472            }
473        }
474        info!(
475            "Claiming static deposit for utxo {}:{}",
476            detailed_utxo.txid, detailed_utxo.vout
477        );
478        let transfer = self.spark_wallet.claim_static_deposit(quote).await?;
479        info!(
480            "Claimed static deposit transfer: {}",
481            serde_json::to_string_pretty(&transfer)?
482        );
483        Ok(transfer)
484    }
485}
486
487#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
488#[allow(clippy::needless_pass_by_value)]
489impl BreezSdk {
490    /// Registers a listener to receive SDK events
491    ///
492    /// # Arguments
493    ///
494    /// * `listener` - An implementation of the `EventListener` trait
495    ///
496    /// # Returns
497    ///
498    /// A unique identifier for the listener, which can be used to remove it later
499    pub fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
500        self.event_emitter.add_listener(listener)
501    }
502
503    /// Removes a previously registered event listener
504    ///
505    /// # Arguments
506    ///
507    /// * `id` - The listener ID returned from `add_event_listener`
508    ///
509    /// # Returns
510    ///
511    /// `true` if the listener was found and removed, `false` otherwise
512    pub fn remove_event_listener(&self, id: &str) -> bool {
513        self.event_emitter.remove_listener(id)
514    }
515
516    /// Stops the SDK's background tasks
517    ///
518    /// This method stops the background tasks started by the `start()` method.
519    /// It should be called before your application terminates to ensure proper cleanup.
520    ///
521    /// # Returns
522    ///
523    /// Result containing either success or an `SdkError` if the background task couldn't be stopped
524    pub fn disconnect(&self) -> Result<(), SdkError> {
525        self.shutdown_sender
526            .send(())
527            .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
528
529        Ok(())
530    }
531
532    /// Returns the balance of the wallet in satoshis
533    #[allow(unused_variables)]
534    pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
535        let object_repository = ObjectCacheRepository::new(self.storage.clone());
536        let account_info = object_repository
537            .fetch_account_info()
538            .await?
539            .unwrap_or_default();
540        Ok(GetInfoResponse {
541            balance_sats: account_info.balance_sats,
542        })
543    }
544
545    pub async fn receive_payment(
546        &self,
547        request: ReceivePaymentRequest,
548    ) -> Result<ReceivePaymentResponse, SdkError> {
549        match &request.payment_method {
550            ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse {
551                fee_sats: 0,
552                payment_request: self.spark_wallet.get_spark_address().await?.to_string(),
553            }),
554            ReceivePaymentMethod::BitcoinAddress => {
555                // TODO: allow passing amount
556
557                let object_repository = ObjectCacheRepository::new(self.storage.clone());
558
559                // First lookup in storage cache
560                let static_deposit_address =
561                    object_repository.fetch_static_deposit_address().await?;
562                if let Some(static_deposit_address) = static_deposit_address {
563                    return Ok(ReceivePaymentResponse {
564                        payment_request: static_deposit_address.address.to_string(),
565                        fee_sats: 0,
566                    });
567                }
568
569                // Then query existing addresses
570                let deposit_addresses = self
571                    .spark_wallet
572                    .list_static_deposit_addresses(None)
573                    .await?;
574
575                // In case there are no addresses, generate a new one and cache it
576                let address = match deposit_addresses.last() {
577                    Some(address) => address.to_string(),
578                    None => self
579                        .spark_wallet
580                        .generate_deposit_address(true)
581                        .await?
582                        .to_string(),
583                };
584
585                object_repository
586                    .save_static_deposit_address(&StaticDepositAddress {
587                        address: address.clone(),
588                    })
589                    .await?;
590
591                Ok(ReceivePaymentResponse {
592                    payment_request: address,
593                    fee_sats: 0,
594                })
595            }
596            ReceivePaymentMethod::Bolt11Invoice {
597                description,
598                amount_sats,
599            } => Ok(ReceivePaymentResponse {
600                payment_request: self
601                    .spark_wallet
602                    .create_lightning_invoice(
603                        amount_sats.unwrap_or_default(),
604                        Some(description.clone()),
605                    )
606                    .await?
607                    .invoice,
608                fee_sats: 0,
609            }),
610        }
611    }
612
613    pub async fn prepare_lnurl_pay(
614        &self,
615        request: PrepareLnurlPayRequest,
616    ) -> Result<PrepareLnurlPayResponse, SdkError> {
617        let success_data = match validate_lnurl_pay(
618            self.lnurl_client.as_ref(),
619            request.amount_sats.saturating_mul(1_000),
620            &None,
621            &request.pay_request,
622            self.config.network.into(),
623            request.validate_success_action_url,
624        )
625        .await?
626        {
627            ValidatedCallbackResponse::EndpointError { data } => {
628                return Err(LnurlError::EndpointError(data.reason).into());
629            }
630            ValidatedCallbackResponse::EndpointSuccess { data } => data,
631        };
632
633        let prepare_response = self
634            .prepare_send_payment(PrepareSendPaymentRequest {
635                payment_request: success_data.pr,
636                amount_sats: Some(request.amount_sats),
637            })
638            .await?;
639
640        let SendPaymentMethod::Bolt11Invoice {
641            invoice_details,
642            lightning_fee_sats,
643            ..
644        } = prepare_response.payment_method
645        else {
646            return Err(SdkError::Generic(
647                "Expected Bolt11Invoice payment method".to_string(),
648            ));
649        };
650
651        Ok(PrepareLnurlPayResponse {
652            amount_sats: request.amount_sats,
653            comment: request.comment,
654            pay_request: request.pay_request,
655            invoice_details,
656            fee_sats: lightning_fee_sats,
657            success_action: success_data.success_action,
658        })
659    }
660
661    pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
662        let mut payment = self
663            .send_payment_internal(
664                SendPaymentRequest {
665                    prepare_response: PrepareSendPaymentResponse {
666                        payment_method: SendPaymentMethod::Bolt11Invoice {
667                            invoice_details: request.prepare_response.invoice_details,
668                            spark_transfer_fee_sats: None,
669                            lightning_fee_sats: request.prepare_response.fee_sats,
670                        },
671                        amount_sats: request.prepare_response.amount_sats,
672                    },
673                    options: None,
674                },
675                true,
676            )
677            .await?
678            .payment;
679
680        let success_action =
681            process_success_action(&payment, request.prepare_response.success_action.as_ref())?;
682
683        let lnurl_info = LnurlPayInfo {
684            ln_address: request.prepare_response.pay_request.address,
685            comment: request.prepare_response.comment,
686            domain: Some(request.prepare_response.pay_request.domain),
687            metadata: Some(request.prepare_response.pay_request.metadata_str),
688            processed_success_action: success_action.clone(),
689            raw_success_action: request.prepare_response.success_action,
690        };
691        let Some(PaymentDetails::Lightning { lnurl_pay_info, .. }) = &mut payment.details else {
692            return Err(SdkError::Generic(
693                "Expected Lightning payment details".to_string(),
694            ));
695        };
696        *lnurl_pay_info = Some(lnurl_info.clone());
697
698        self.storage
699            .set_payment_metadata(
700                payment.id.clone(),
701                PaymentMetadata {
702                    lnurl_pay_info: Some(lnurl_info),
703                },
704            )
705            .await?;
706        self.event_emitter.emit(&SdkEvent::PaymentSucceeded {
707            payment: payment.clone(),
708        });
709        Ok(LnurlPayResponse {
710            payment,
711            success_action,
712        })
713    }
714
715    pub async fn prepare_send_payment(
716        &self,
717        request: PrepareSendPaymentRequest,
718    ) -> Result<PrepareSendPaymentResponse, SdkError> {
719        // First check for spark address
720        if let Ok(spark_address) = request.payment_request.parse::<SparkAddress>() {
721            return Ok(PrepareSendPaymentResponse {
722                payment_method: SendPaymentMethod::SparkAddress {
723                    address: spark_address.to_string(),
724                    fee_sats: 0,
725                },
726                amount_sats: request
727                    .amount_sats
728                    .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
729            });
730        }
731        // Then check for other types of inputs
732        let parsed_input = parse(&request.payment_request).await?;
733        match &parsed_input {
734            InputType::Bolt11Invoice(detailed_bolt11_invoice) => {
735                let spark_address = self
736                    .spark_wallet
737                    .extract_spark_address(&request.payment_request)?;
738
739                let spark_transfer_fee_sats = if spark_address.is_some() {
740                    Some(0)
741                } else {
742                    None
743                };
744
745                let lightning_fee_sats = self
746                    .spark_wallet
747                    .fetch_lightning_send_fee_estimate(
748                        &request.payment_request,
749                        request.amount_sats,
750                    )
751                    .await?;
752
753                Ok(PrepareSendPaymentResponse {
754                    payment_method: SendPaymentMethod::Bolt11Invoice {
755                        invoice_details: detailed_bolt11_invoice.clone(),
756                        spark_transfer_fee_sats,
757                        lightning_fee_sats,
758                    },
759                    amount_sats: request
760                        .amount_sats
761                        .or(detailed_bolt11_invoice.amount_msat.map(|msat| msat / 1000))
762                        .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
763                })
764            }
765            InputType::BitcoinAddress(withdrawal_address) => {
766                let fee_quote = self
767                    .spark_wallet
768                    .fetch_coop_exit_fee_quote(
769                        &withdrawal_address.address,
770                        Some(
771                            request
772                                .amount_sats
773                                .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
774                        ),
775                    )
776                    .await?;
777                Ok(PrepareSendPaymentResponse {
778                    payment_method: SendPaymentMethod::BitcoinAddress {
779                        address: withdrawal_address.clone(),
780                        fee_quote: fee_quote.into(),
781                    },
782                    amount_sats: request
783                        .amount_sats
784                        .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
785                })
786            }
787            _ => Err(SdkError::InvalidInput(
788                "Unsupported payment method".to_string(),
789            )),
790        }
791    }
792
793    pub async fn send_payment(
794        &self,
795        request: SendPaymentRequest,
796    ) -> Result<SendPaymentResponse, SdkError> {
797        self.send_payment_internal(request, false).await
798    }
799
800    async fn send_payment_internal(
801        &self,
802        request: SendPaymentRequest,
803        suppress_payment_event: bool,
804    ) -> Result<SendPaymentResponse, SdkError> {
805        let res = match request.prepare_response.payment_method {
806            SendPaymentMethod::SparkAddress { address, .. } => {
807                let spark_address = address
808                    .parse::<SparkAddress>()
809                    .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
810                let transfer = self
811                    .spark_wallet
812                    .transfer(request.prepare_response.amount_sats, &spark_address)
813                    .await?;
814                Ok(SendPaymentResponse {
815                    payment: transfer.try_into()?,
816                })
817            }
818            SendPaymentMethod::Bolt11Invoice {
819                invoice_details,
820                spark_transfer_fee_sats,
821                lightning_fee_sats,
822            } => {
823                let amount_to_send = match invoice_details.amount_msat {
824                    // we are not sending amount in case the invoice contains it.
825                    Some(_) => None,
826                    // We are sending amount for zero amount invoice
827                    None => Some(request.prepare_response.amount_sats),
828                };
829                let use_spark = match request.options {
830                    Some(SendPaymentOptions::Bolt11Invoice { use_spark }) => use_spark,
831                    _ => false,
832                };
833                let fee_sats = match (use_spark, spark_transfer_fee_sats, lightning_fee_sats) {
834                    (true, Some(fee), _) => fee,
835                    _ => lightning_fee_sats,
836                };
837                if use_spark && spark_transfer_fee_sats.is_none() {
838                    return Err(SdkError::InvalidInput(
839                        "Cannot use spark to pay invoice as it doesn't contain a spark address"
840                            .to_string(),
841                    ));
842                }
843
844                let payment_response = self
845                    .spark_wallet
846                    .pay_lightning_invoice(
847                        &invoice_details.invoice.bolt11,
848                        amount_to_send,
849                        Some(fee_sats),
850                        use_spark,
851                    )
852                    .await?;
853                let payment = match payment_response {
854                    PayLightningInvoiceResult::LightningPayment(payment) => {
855                        self.poll_lightning_send_payment(&payment.id);
856                        Payment::from_lightning(payment, request.prepare_response.amount_sats)?
857                    }
858                    PayLightningInvoiceResult::Transfer(payment) => payment.try_into()?,
859                };
860                Ok(SendPaymentResponse { payment })
861            }
862            SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
863                let exit_speed: ExitSpeed = match request.options {
864                    Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) => {
865                        confirmation_speed.into()
866                    }
867                    None => ExitSpeed::Fast,
868                    _ => {
869                        return Err(SdkError::InvalidInput("Invalid options".to_string()));
870                    }
871                };
872                let response = self
873                    .spark_wallet
874                    .withdraw(
875                        &address.address,
876                        Some(request.prepare_response.amount_sats),
877                        exit_speed,
878                        fee_quote.into(),
879                    )
880                    .await?;
881                Ok(SendPaymentResponse {
882                    payment: response.try_into()?,
883                })
884            }
885        };
886        if let Ok(response) = &res {
887            //TODO: We get incomplete payments here from the ssp so better not to persist for now.
888            // we trigger the sync here anyway to get the fresh payment.
889            //self.storage.insert_payment(response.payment.clone()).await?;
890            if !suppress_payment_event {
891                self.event_emitter.emit(&SdkEvent::PaymentSucceeded {
892                    payment: response.payment.clone(),
893                });
894            }
895            if let Err(e) = self.sync_trigger.send(SyncType::PaymentsOnly) {
896                error!("Failed to send sync trigger: {e:?}");
897            }
898        }
899        res
900    }
901
902    // Pools the lightning send payment untill it is in completed state.
903    fn poll_lightning_send_payment(&self, payment_id: &str) {
904        const MAX_POLL_ATTEMPTS: u32 = 10;
905        info!("Polling lightning send payment {}", payment_id);
906
907        let spark_wallet = self.spark_wallet.clone();
908        let sync_trigger = self.sync_trigger.clone();
909        let payment_id = payment_id.to_string();
910        let mut shutdown = self.shutdown_receiver.clone();
911
912        tokio::spawn(async move {
913            for i in 0..MAX_POLL_ATTEMPTS {
914                info!(
915                    "Polling lightning send payment {} attempt {}",
916                    payment_id, i
917                );
918                select! {
919                  _ = shutdown.changed() => {
920                    info!("Shutdown signal received");
921                    return;
922                  },
923                    p = spark_wallet.fetch_lightning_send_payment(&payment_id) => {
924                      if let Ok(Some(p)) = p  && p.payment_preimage.is_some(){
925                          info!("Pollling payment preimage found");
926                          if let Err(e) = sync_trigger.send(SyncType::PaymentsOnly) {
927                              error!("Failed to send sync trigger: {e:?}");
928                          }
929                          return;
930                    }
931                    let sleep_time = if i < 5 {
932                        Duration::from_secs(1)
933                    } else {
934                        Duration::from_secs(i.into())
935                    };
936                    tokio::time::sleep(sleep_time).await;
937                  }
938                }
939            }
940        });
941    }
942
943    /// Synchronizes the wallet with the Spark network
944    #[allow(unused_variables)]
945    pub fn sync_wallet(&self, request: SyncWalletRequest) -> Result<SyncWalletResponse, SdkError> {
946        if let Err(e) = self.sync_trigger.send(SyncType::Full) {
947            error!("Failed to send sync trigger: {e:?}");
948        }
949        Ok(SyncWalletResponse {})
950    }
951
952    /// Lists payments from the storage with pagination
953    ///
954    /// This method provides direct access to the payment history stored in the database.
955    /// It returns payments in reverse chronological order (newest first).
956    ///
957    /// # Arguments
958    ///
959    /// * `request` - Contains pagination parameters (offset and limit)
960    ///
961    /// # Returns
962    ///
963    /// * `Ok(ListPaymentsResponse)` - Contains the list of payments if successful
964    /// * `Err(SdkError)` - If there was an error accessing the storage
965    ///
966    pub async fn list_payments(
967        &self,
968        request: ListPaymentsRequest,
969    ) -> Result<ListPaymentsResponse, SdkError> {
970        let payments = self
971            .storage
972            .list_payments(request.offset, request.limit)
973            .await?;
974        Ok(ListPaymentsResponse { payments })
975    }
976
977    pub async fn get_payment(
978        &self,
979        request: GetPaymentRequest,
980    ) -> Result<GetPaymentResponse, SdkError> {
981        let payment = self.storage.get_payment_by_id(request.payment_id).await?;
982        Ok(GetPaymentResponse { payment })
983    }
984
985    pub async fn claim_deposit(
986        &self,
987        request: ClaimDepositRequest,
988    ) -> Result<ClaimDepositResponse, SdkError> {
989        let detailed_utxo =
990            CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
991                .fetch_detailed_utxo(&request.txid, request.vout)
992                .await?;
993
994        let max_fee = request
995            .max_fee
996            .or(self.config.max_deposit_claim_fee.clone());
997        match self.claim_utxo(&detailed_utxo, max_fee).await {
998            Ok(transfer) => {
999                self.storage
1000                    .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
1001                    .await?;
1002                if let Err(e) = self.sync_trigger.send(SyncType::PaymentsOnly) {
1003                    error!("Failed to execute sync after deposit claim: {e:?}");
1004                }
1005                Ok(ClaimDepositResponse {
1006                    payment: transfer.try_into()?,
1007                })
1008            }
1009            Err(e) => {
1010                error!("Failed to claim deposit: {e:?}");
1011                self.storage
1012                    .update_deposit(
1013                        detailed_utxo.txid.to_string(),
1014                        detailed_utxo.vout,
1015                        UpdateDepositPayload::ClaimError {
1016                            error: e.clone().into(),
1017                        },
1018                    )
1019                    .await?;
1020                Err(e)
1021            }
1022        }
1023    }
1024
1025    pub async fn refund_deposit(
1026        &self,
1027        request: RefundDepositRequest,
1028    ) -> Result<RefundDepositResponse, SdkError> {
1029        let detailed_utxo =
1030            CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1031                .fetch_detailed_utxo(&request.txid, request.vout)
1032                .await?;
1033        let tx = self
1034            .spark_wallet
1035            .refund_static_deposit(
1036                detailed_utxo.clone().tx,
1037                Some(detailed_utxo.vout),
1038                &request.destination_address,
1039                request.fee.into(),
1040            )
1041            .await?;
1042        let deposit: DepositInfo = detailed_utxo.into();
1043        let tx_hex = serialize(&tx).as_hex().to_string();
1044        let tx_id = tx.compute_txid().to_string();
1045
1046        // Store the refund transaction details separately
1047        self.storage
1048            .update_deposit(
1049                deposit.txid.clone(),
1050                deposit.vout,
1051                UpdateDepositPayload::Refund {
1052                    refund_tx: tx_hex.clone(),
1053                    refund_txid: tx_id.clone(),
1054                },
1055            )
1056            .await?;
1057
1058        self.chain_service
1059            .broadcast_transaction(tx_hex.clone())
1060            .await?;
1061        Ok(RefundDepositResponse { tx_id, tx_hex })
1062    }
1063
1064    #[allow(unused_variables)]
1065    pub async fn list_unclaimed_deposits(
1066        &self,
1067        request: ListUnclaimedDepositsRequest,
1068    ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
1069        let deposits = self.storage.list_deposits().await?;
1070        Ok(ListUnclaimedDepositsResponse { deposits })
1071    }
1072}
1073
1074fn process_success_action(
1075    payment: &Payment,
1076    success_action: Option<&SuccessAction>,
1077) -> Result<Option<SuccessActionProcessed>, LnurlError> {
1078    let Some(success_action) = success_action else {
1079        return Ok(None);
1080    };
1081
1082    let data = match success_action {
1083        SuccessAction::Aes { data } => data,
1084        SuccessAction::Message { data } => {
1085            return Ok(Some(SuccessActionProcessed::Message { data: data.clone() }));
1086        }
1087        SuccessAction::Url { data } => {
1088            return Ok(Some(SuccessActionProcessed::Url { data: data.clone() }));
1089        }
1090    };
1091
1092    let Some(PaymentDetails::Lightning { preimage, .. }) = &payment.details else {
1093        return Err(LnurlError::general(format!(
1094            "Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.",
1095            payment.details
1096        )));
1097    };
1098
1099    let Some(preimage) = preimage else {
1100        return Ok(None);
1101    };
1102
1103    let preimage =
1104        sha256::Hash::from_str(preimage).map_err(|_| LnurlError::general("Invalid preimage"))?;
1105    let preimage = preimage.as_byte_array();
1106    let result: AesSuccessActionDataResult = match (data, preimage).try_into() {
1107        Ok(data) => AesSuccessActionDataResult::Decrypted { data },
1108        Err(e) => AesSuccessActionDataResult::ErrorStatus {
1109            reason: e.to_string(),
1110        },
1111    };
1112
1113    Ok(Some(SuccessActionProcessed::Aes { result }))
1114}
1115
1116fn validate_breez_api_key(api_key: &str) -> Result<(), SdkError> {
1117    let api_key_decoded = base64::engine::general_purpose::STANDARD
1118        .decode(api_key.as_bytes())
1119        .map_err(|err| {
1120            SdkError::Generic(format!(
1121                "Could not base64 decode the Breez API key: {err:?}"
1122            ))
1123        })?;
1124    let (_rem, cert) = parse_x509_certificate(&api_key_decoded).map_err(|err| {
1125        SdkError::Generic(format!("Invaid certificate for Breez API key: {err:?}"))
1126    })?;
1127
1128    let issuer = cert
1129        .issuer()
1130        .iter_common_name()
1131        .next()
1132        .and_then(|cn| cn.as_str().ok());
1133    match issuer {
1134        Some(common_name) => {
1135            if !common_name.starts_with("Breez") {
1136                return Err(SdkError::Generic(
1137                    "Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted"
1138                        .to_string()
1139                ));
1140            }
1141        }
1142        _ => {
1143            return Err(SdkError::Generic(
1144                "Could not parse Breez API key certificate: issuer is invalid or not found."
1145                    .to_string(),
1146            ));
1147        }
1148    }
1149
1150    Ok(())
1151}