breez_sdk_spark/sdk/
api.rs

1use bitcoin::secp256k1::{PublicKey, ecdsa::Signature};
2use std::str::FromStr;
3use tracing::{error, info};
4
5use crate::{
6    BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse,
7    GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, ListFiatCurrenciesResponse,
8    ListFiatRatesResponse, OptimizationProgress, SignMessageRequest, SignMessageResponse,
9    UpdateUserSettingsRequest, UserSettings,
10    chain::RecommendedFees,
11    error::SdkError,
12    events::EventListener,
13    issuer::TokenIssuer,
14    models::{GetInfoRequest, GetInfoResponse},
15    persist::ObjectCacheRepository,
16    utils::token::get_tokens_metadata_cached_or_query,
17};
18
19use super::{BreezSdk, helpers::get_or_create_deposit_address, parse_input};
20
21#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
22#[allow(clippy::needless_pass_by_value)]
23impl BreezSdk {
24    /// Registers a listener to receive SDK events
25    ///
26    /// # Arguments
27    ///
28    /// * `listener` - An implementation of the `EventListener` trait
29    ///
30    /// # Returns
31    ///
32    /// A unique identifier for the listener, which can be used to remove it later
33    pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
34        self.event_emitter.add_listener(listener).await
35    }
36
37    /// Removes a previously registered event listener
38    ///
39    /// # Arguments
40    ///
41    /// * `id` - The listener ID returned from `add_event_listener`
42    ///
43    /// # Returns
44    ///
45    /// `true` if the listener was found and removed, `false` otherwise
46    pub async fn remove_event_listener(&self, id: &str) -> bool {
47        self.event_emitter.remove_listener(id).await
48    }
49
50    /// Stops the SDK's background tasks
51    ///
52    /// This method stops the background tasks started by the `start()` method.
53    /// It should be called before your application terminates to ensure proper cleanup.
54    ///
55    /// # Returns
56    ///
57    /// Result containing either success or an `SdkError` if the background task couldn't be stopped
58    pub async fn disconnect(&self) -> Result<(), SdkError> {
59        info!("Disconnecting Breez SDK");
60        self.shutdown_sender
61            .send(())
62            .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
63
64        self.shutdown_sender.closed().await;
65        info!("Breez SDK disconnected");
66        Ok(())
67    }
68
69    pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
70        parse_input(input, Some(self.external_input_parsers.clone())).await
71    }
72
73    /// Returns the balance of the wallet in satoshis
74    #[allow(unused_variables)]
75    pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
76        if request.ensure_synced.unwrap_or_default() {
77            self.initial_synced_watcher
78                .clone()
79                .changed()
80                .await
81                .map_err(|_| {
82                    SdkError::Generic("Failed to receive initial synced signal".to_string())
83                })?;
84        }
85        let object_repository = ObjectCacheRepository::new(self.storage.clone());
86        let account_info = object_repository
87            .fetch_account_info()
88            .await?
89            .unwrap_or_default();
90        Ok(GetInfoResponse {
91            identity_pubkey: self.spark_wallet.get_identity_public_key().to_string(),
92            balance_sats: account_info.balance_sats,
93            token_balances: account_info.token_balances,
94        })
95    }
96
97    /// List fiat currencies for which there is a known exchange rate,
98    /// sorted by the canonical name of the currency.
99    pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
100        let currencies = self
101            .fiat_service
102            .fetch_fiat_currencies()
103            .await?
104            .into_iter()
105            .map(From::from)
106            .collect();
107        Ok(ListFiatCurrenciesResponse { currencies })
108    }
109
110    /// List the latest rates of fiat currencies, sorted by name.
111    pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
112        let rates = self
113            .fiat_service
114            .fetch_fiat_rates()
115            .await?
116            .into_iter()
117            .map(From::from)
118            .collect();
119        Ok(ListFiatRatesResponse { rates })
120    }
121
122    /// Get the recommended BTC fees based on the configured chain service.
123    pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
124        Ok(self.chain_service.recommended_fees().await?)
125    }
126
127    /// Returns the metadata for the given token identifiers.
128    ///
129    /// Results are not guaranteed to be in the same order as the input token identifiers.
130    ///
131    /// If the metadata is not found locally in cache, it will be queried from
132    /// the Spark network and then cached.
133    pub async fn get_tokens_metadata(
134        &self,
135        request: GetTokensMetadataRequest,
136    ) -> Result<GetTokensMetadataResponse, SdkError> {
137        let metadata = get_tokens_metadata_cached_or_query(
138            &self.spark_wallet,
139            &ObjectCacheRepository::new(self.storage.clone()),
140            &request
141                .token_identifiers
142                .iter()
143                .map(String::as_str)
144                .collect::<Vec<_>>(),
145        )
146        .await?;
147        Ok(GetTokensMetadataResponse {
148            tokens_metadata: metadata,
149        })
150    }
151
152    /// Signs a message with the wallet's identity key. The message is SHA256
153    /// hashed before signing. The returned signature will be hex encoded in
154    /// DER format by default, or compact format if specified.
155    pub async fn sign_message(
156        &self,
157        request: SignMessageRequest,
158    ) -> Result<SignMessageResponse, SdkError> {
159        use bitcoin::hex::DisplayHex;
160
161        let pubkey = self.spark_wallet.get_identity_public_key().to_string();
162        let signature = self.spark_wallet.sign_message(&request.message).await?;
163        let signature_hex = if request.compact {
164            signature.serialize_compact().to_lower_hex_string()
165        } else {
166            signature.serialize_der().to_lower_hex_string()
167        };
168
169        Ok(SignMessageResponse {
170            pubkey,
171            signature: signature_hex,
172        })
173    }
174
175    /// Verifies a message signature against the provided public key. The message
176    /// is SHA256 hashed before verification. The signature can be hex encoded
177    /// in either DER or compact format.
178    pub async fn check_message(
179        &self,
180        request: CheckMessageRequest,
181    ) -> Result<CheckMessageResponse, SdkError> {
182        let pubkey = PublicKey::from_str(&request.pubkey)
183            .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
184        let signature_bytes = hex::decode(&request.signature)
185            .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
186        let signature = Signature::from_der(&signature_bytes)
187            .or_else(|_| Signature::from_compact(&signature_bytes))
188            .map_err(|_| {
189                SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
190            })?;
191
192        let is_valid = self
193            .spark_wallet
194            .verify_message(&request.message, &signature, &pubkey)
195            .await
196            .is_ok();
197        Ok(CheckMessageResponse { is_valid })
198    }
199
200    /// Returns the user settings for the wallet.
201    ///
202    /// Some settings are fetched from the Spark network so network requests are performed.
203    pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
204        // Ensure spark private mode is initialized to avoid race conditions with the initialization task.
205        self.ensure_spark_private_mode_initialized().await?;
206
207        let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
208
209        // We may in the future have user settings that are stored locally and synced using real-time sync.
210
211        Ok(UserSettings {
212            spark_private_mode_enabled: spark_user_settings.private_enabled,
213        })
214    }
215
216    /// Updates the user settings for the wallet.
217    ///
218    /// Some settings are updated on the Spark network so network requests may be performed.
219    pub async fn update_user_settings(
220        &self,
221        request: UpdateUserSettingsRequest,
222    ) -> Result<(), SdkError> {
223        if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
224            self.spark_wallet
225                .update_wallet_settings(spark_private_mode_enabled)
226                .await?;
227
228            // Reregister the lightning address if spark private mode changed.
229            let lightning_address = match self.get_lightning_address().await {
230                Ok(lightning_address) => lightning_address,
231                Err(e) => {
232                    error!("Failed to get lightning address during user settings update: {e:?}");
233                    return Ok(());
234                }
235            };
236            let Some(lightning_address) = lightning_address else {
237                return Ok(());
238            };
239            if let Err(e) = self
240                .register_lightning_address_internal(crate::RegisterLightningAddressRequest {
241                    username: lightning_address.username,
242                    description: Some(lightning_address.description),
243                })
244                .await
245            {
246                error!("Failed to reregister lightning address during user settings update: {e:?}");
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 fn start_leaf_optimization(&self) {
263        self.spark_wallet.start_leaf_optimization();
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    /// Initiates a Bitcoin purchase flow via an external provider (`MoonPay`).
285    ///
286    /// This method generates a URL that the user can open in a browser to complete
287    /// the Bitcoin purchase. The purchased Bitcoin will be sent to an automatically
288    /// generated deposit address.
289    ///
290    /// # Arguments
291    ///
292    /// * `request` - The purchase request containing optional amount and redirect URL
293    ///
294    /// # Returns
295    ///
296    /// A response containing the URL to open in a browser to complete the purchase
297    pub async fn buy_bitcoin(
298        &self,
299        request: BuyBitcoinRequest,
300    ) -> Result<BuyBitcoinResponse, SdkError> {
301        let address =
302            get_or_create_deposit_address(&self.spark_wallet, self.storage.clone(), true).await?;
303
304        let url = self
305            .buy_bitcoin_provider
306            .buy_bitcoin(address, request.locked_amount_sat, request.redirect_url)
307            .await
308            .map_err(|e| SdkError::Generic(format!("Failed to create buy bitcoin URL: {e}")))?;
309
310        Ok(BuyBitcoinResponse { url })
311    }
312}