breez_sdk_spark/sdk/
api.rs

1use bitcoin::secp256k1::{PublicKey, ecdsa::Signature};
2use std::str::FromStr;
3use tracing::info;
4
5use breez_sdk_common::buy::cashapp::CashAppProvider;
6
7use crate::{
8    BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse,
9    GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, ListFiatCurrenciesResponse,
10    ListFiatRatesResponse, Network, OptimizationProgress, RegisterWebhookRequest,
11    RegisterWebhookResponse, SignMessageRequest, SignMessageResponse, UnregisterWebhookRequest,
12    UpdateUserSettingsRequest, UserSettings, Webhook,
13    chain::RecommendedFees,
14    error::SdkError,
15    events::EventListener,
16    issuer::TokenIssuer,
17    models::{GetInfoRequest, GetInfoResponse, StableBalanceActiveLabel},
18    persist::ObjectCacheRepository,
19    utils::token::get_tokens_metadata_cached_or_query,
20};
21
22use super::{BreezSdk, helpers::get_deposit_address, parse_input};
23
24#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
25#[allow(clippy::needless_pass_by_value)]
26impl BreezSdk {
27    /// Registers a listener to receive SDK events
28    ///
29    /// # Arguments
30    ///
31    /// * `listener` - An implementation of the `EventListener` trait
32    ///
33    /// # Returns
34    ///
35    /// A unique identifier for the listener, which can be used to remove it later
36    pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
37        self.event_emitter.add_external_listener(listener).await
38    }
39
40    /// Removes a previously registered event listener
41    ///
42    /// # Arguments
43    ///
44    /// * `id` - The listener ID returned from `add_event_listener`
45    ///
46    /// # Returns
47    ///
48    /// `true` if the listener was found and removed, `false` otherwise
49    pub async fn remove_event_listener(&self, id: &str) -> bool {
50        self.event_emitter.remove_external_listener(id).await
51    }
52
53    /// Stops the SDK's background tasks
54    ///
55    /// This method stops the background tasks started by the `start()` method.
56    /// It should be called before your application terminates to ensure proper cleanup.
57    ///
58    /// # Returns
59    ///
60    /// Result containing either success or an `SdkError` if the background task couldn't be stopped
61    pub async fn disconnect(&self) -> Result<(), SdkError> {
62        info!("Disconnecting Breez SDK");
63        self.shutdown_sender
64            .send(())
65            .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
66
67        self.shutdown_sender.closed().await;
68        info!("Breez SDK disconnected");
69        Ok(())
70    }
71
72    pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
73        parse_input(input, Some(self.external_input_parsers.clone())).await
74    }
75
76    /// Returns the balance of the wallet in satoshis
77    #[allow(unused_variables)]
78    pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
79        if request.ensure_synced.unwrap_or_default() {
80            self.initial_synced_watcher
81                .clone()
82                .changed()
83                .await
84                .map_err(|_| {
85                    SdkError::Generic("Failed to receive initial synced signal".to_string())
86                })?;
87        }
88        let object_repository = ObjectCacheRepository::new(self.storage.clone());
89        let account_info = object_repository
90            .fetch_account_info()
91            .await?
92            .unwrap_or_default();
93        Ok(GetInfoResponse {
94            identity_pubkey: self.spark_wallet.get_identity_public_key().to_string(),
95            balance_sats: account_info.balance_sats,
96            token_balances: account_info.token_balances,
97        })
98    }
99
100    /// List fiat currencies for which there is a known exchange rate,
101    /// sorted by the canonical name of the currency.
102    pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
103        let currencies = self
104            .fiat_service
105            .fetch_fiat_currencies()
106            .await?
107            .into_iter()
108            .map(From::from)
109            .collect();
110        Ok(ListFiatCurrenciesResponse { currencies })
111    }
112
113    /// List the latest rates of fiat currencies, sorted by name.
114    pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
115        let rates = self
116            .fiat_service
117            .fetch_fiat_rates()
118            .await?
119            .into_iter()
120            .map(From::from)
121            .collect();
122        Ok(ListFiatRatesResponse { rates })
123    }
124
125    /// Get the recommended BTC fees based on the configured chain service.
126    pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
127        Ok(self.chain_service.recommended_fees().await?)
128    }
129
130    /// Returns the metadata for the given token identifiers.
131    ///
132    /// Results are not guaranteed to be in the same order as the input token identifiers.
133    ///
134    /// If the metadata is not found locally in cache, it will be queried from
135    /// the Spark network and then cached.
136    pub async fn get_tokens_metadata(
137        &self,
138        request: GetTokensMetadataRequest,
139    ) -> Result<GetTokensMetadataResponse, SdkError> {
140        let metadata = get_tokens_metadata_cached_or_query(
141            &self.spark_wallet,
142            &ObjectCacheRepository::new(self.storage.clone()),
143            &request
144                .token_identifiers
145                .iter()
146                .map(String::as_str)
147                .collect::<Vec<_>>(),
148        )
149        .await?;
150        Ok(GetTokensMetadataResponse {
151            tokens_metadata: metadata,
152        })
153    }
154
155    /// Signs a message with the wallet's identity key. The message is SHA256
156    /// hashed before signing. The returned signature will be hex encoded in
157    /// DER format by default, or compact format if specified.
158    pub async fn sign_message(
159        &self,
160        request: SignMessageRequest,
161    ) -> Result<SignMessageResponse, SdkError> {
162        use bitcoin::hex::DisplayHex;
163
164        let pubkey = self.spark_wallet.get_identity_public_key().to_string();
165        let signature = self.spark_wallet.sign_message(&request.message).await?;
166        let signature_hex = if request.compact {
167            signature.serialize_compact().to_lower_hex_string()
168        } else {
169            signature.serialize_der().to_lower_hex_string()
170        };
171
172        Ok(SignMessageResponse {
173            pubkey,
174            signature: signature_hex,
175        })
176    }
177
178    /// Verifies a message signature against the provided public key. The message
179    /// is SHA256 hashed before verification. The signature can be hex encoded
180    /// in either DER or compact format.
181    pub async fn check_message(
182        &self,
183        request: CheckMessageRequest,
184    ) -> Result<CheckMessageResponse, SdkError> {
185        let pubkey = PublicKey::from_str(&request.pubkey)
186            .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
187        let signature_bytes = hex::decode(&request.signature)
188            .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
189        let signature = Signature::from_der(&signature_bytes)
190            .or_else(|_| Signature::from_compact(&signature_bytes))
191            .map_err(|_| {
192                SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
193            })?;
194
195        let is_valid = self
196            .spark_wallet
197            .verify_message(&request.message, &signature, &pubkey)
198            .await
199            .is_ok();
200        Ok(CheckMessageResponse { is_valid })
201    }
202
203    /// Returns the user settings for the wallet.
204    ///
205    /// Some settings are fetched from the Spark network so network requests are performed.
206    pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
207        // Ensure spark private mode is initialized to avoid race conditions with the initialization task.
208        self.ensure_spark_private_mode_initialized().await?;
209
210        let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
211
212        let stable_balance_active_label = match &self.stable_balance {
213            Some(sb) => sb.get_active_label().await,
214            None => None,
215        };
216
217        Ok(UserSettings {
218            spark_private_mode_enabled: spark_user_settings.private_enabled,
219            stable_balance_active_label,
220        })
221    }
222
223    /// Updates the user settings for the wallet.
224    ///
225    /// Some settings are updated on the Spark network so network requests may be performed.
226    pub async fn update_user_settings(
227        &self,
228        request: UpdateUserSettingsRequest,
229    ) -> Result<(), SdkError> {
230        if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
231            self.spark_wallet
232                .update_wallet_settings(spark_private_mode_enabled)
233                .await?;
234        }
235
236        if let Some(active_label) = request.stable_balance_active_label {
237            let sb = self
238                .stable_balance
239                .as_ref()
240                .ok_or_else(|| SdkError::Generic("Stable balance is not configured".to_string()))?;
241            let label = if let StableBalanceActiveLabel::Set { label } = active_label {
242                Some(label)
243            } else {
244                None
245            };
246            sb.set_active_token(label).await?;
247        }
248
249        Ok(())
250    }
251
252    /// Returns an instance of the [`TokenIssuer`] for managing token issuance.
253    pub fn get_token_issuer(&self) -> TokenIssuer {
254        TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
255    }
256
257    /// Starts leaf optimization in the background.
258    ///
259    /// This method spawns the optimization work in a background task and returns
260    /// immediately. Progress is reported via events.
261    /// If optimization is already running, no new task will be started.
262    pub async fn start_leaf_optimization(&self) {
263        self.spark_wallet.start_leaf_optimization().await;
264    }
265
266    /// Cancels the ongoing leaf optimization.
267    ///
268    /// This method cancels the ongoing optimization and waits for it to fully stop.
269    /// The current round will complete before stopping. This method blocks
270    /// until the optimization has fully stopped and leaves reserved for optimization
271    /// are available again.
272    ///
273    /// If no optimization is running, this method returns immediately.
274    pub async fn cancel_leaf_optimization(&self) -> Result<(), SdkError> {
275        self.spark_wallet.cancel_leaf_optimization().await?;
276        Ok(())
277    }
278
279    /// Returns the current optimization progress snapshot.
280    pub fn get_leaf_optimization_progress(&self) -> OptimizationProgress {
281        self.spark_wallet.get_leaf_optimization_progress().into()
282    }
283
284    /// Registers a webhook to receive notifications for wallet events.
285    ///
286    /// When registered events occur (e.g., a Lightning payment is received),
287    /// the Spark service provider will send an HTTP POST to the specified URL
288    /// with a payload signed using HMAC-SHA256 with the provided secret.
289    ///
290    /// # Arguments
291    ///
292    /// * `request` - The webhook registration details including URL, secret, and event types
293    ///
294    /// # Returns
295    ///
296    /// A response containing the unique identifier of the registered webhook
297    pub async fn register_webhook(
298        &self,
299        request: RegisterWebhookRequest,
300    ) -> Result<RegisterWebhookResponse, SdkError> {
301        let event_types = request.event_types.into_iter().map(Into::into).collect();
302        let webhook_id = self
303            .spark_wallet
304            .register_wallet_webhook(&request.url, &request.secret, event_types)
305            .await
306            .map_err(|e| SdkError::Generic(format!("Failed to register webhook: {e}")))?;
307        Ok(RegisterWebhookResponse { webhook_id })
308    }
309
310    /// Unregisters a previously registered webhook.
311    ///
312    /// After unregistering, the Spark service provider will no longer send
313    /// notifications to the webhook URL.
314    ///
315    /// # Arguments
316    ///
317    /// * `request` - The unregister request containing the webhook ID
318    pub async fn unregister_webhook(
319        &self,
320        request: UnregisterWebhookRequest,
321    ) -> Result<(), SdkError> {
322        self.spark_wallet
323            .delete_wallet_webhook(&request.webhook_id)
324            .await
325            .map_err(|e| SdkError::Generic(format!("Failed to unregister webhook: {e}")))?;
326        Ok(())
327    }
328
329    /// Lists all webhooks currently registered for this wallet.
330    ///
331    /// # Returns
332    ///
333    /// A list of registered webhooks with their IDs, URLs, and subscribed event types
334    pub async fn list_webhooks(&self) -> Result<Vec<Webhook>, SdkError> {
335        let webhooks = self
336            .spark_wallet
337            .list_wallet_webhooks()
338            .await
339            .map_err(|e| SdkError::Generic(format!("Failed to list webhooks: {e}")))?;
340        Ok(webhooks.into_iter().map(Into::into).collect())
341    }
342
343    /// Initiates a Bitcoin purchase flow via an external provider.
344    ///
345    /// Returns a URL the user should open to complete the purchase.
346    /// The request variant determines the provider and its parameters:
347    ///
348    /// - [`BuyBitcoinRequest::Moonpay`]: Fiat-to-Bitcoin via on-chain deposit.
349    /// - [`BuyBitcoinRequest::CashApp`]: Lightning invoice + `cash.app` deep link (mainnet only).
350    pub async fn buy_bitcoin(
351        &self,
352        request: BuyBitcoinRequest,
353    ) -> Result<BuyBitcoinResponse, SdkError> {
354        let url = match request {
355            BuyBitcoinRequest::Moonpay {
356                locked_amount_sat,
357                redirect_url,
358            } => {
359                let address = get_deposit_address(&self.spark_wallet, true).await?;
360                self.buy_bitcoin_provider
361                    .buy_bitcoin(address, locked_amount_sat, redirect_url)
362                    .await
363                    .map_err(|e| {
364                        SdkError::Generic(format!("Failed to create buy bitcoin URL: {e}"))
365                    })?
366            }
367            BuyBitcoinRequest::CashApp { amount_sats } => {
368                if !matches!(self.config.network, Network::Mainnet) {
369                    return Err(SdkError::Generic(
370                        "CashApp is only available on mainnet".to_string(),
371                    ));
372                }
373                let receive_response = self
374                    .receive_bolt11_invoice(
375                        "Buy Bitcoin via CashApp".to_string(),
376                        amount_sats,
377                        None,
378                        None,
379                    )
380                    .await?;
381                CashAppProvider::build_url(&receive_response.payment_request)
382            }
383        };
384
385        Ok(BuyBitcoinResponse { url })
386    }
387}