Skip to main content

breez_sdk_spark/sdk/
api.rs

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