1use bitcoin::secp256k1::{PublicKey, ecdsa::Signature};
2use breez_sdk_common::buy::cashapp::CashAppProvider;
3use std::str::FromStr;
4use tracing::{debug, info};
5
6use crate::{
7 BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse,
8 CrossChainRouteFilter, CrossChainRoutePair, GetTokensMetadataRequest,
9 GetTokensMetadataResponse, InputType, ListFiatCurrenciesResponse, ListFiatRatesResponse,
10 Network, OptimizationMode, OptimizeLeavesRequest, OptimizeLeavesResponse,
11 RegisterWebhookRequest, RegisterWebhookResponse, SignMessageRequest, SignMessageResponse,
12 UnregisterWebhookRequest, 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 pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
42 self.event_emitter.add_external_listener(listener).await
43 }
44
45 pub async fn remove_event_listener(&self, id: &str) -> bool {
55 self.event_emitter.remove_external_listener(id).await
56 }
57
58 pub async fn disconnect(&self) -> Result<(), SdkError> {
70 info!("Disconnecting Breez SDK");
71 self.event_emitter.clear_external_listeners().await;
72 if self.shutdown_sender.send(()).is_err() {
73 debug!("No shutdown receivers; SDK has no background tasks to stop");
79 return Ok(());
80 }
81
82 self.shutdown_sender.closed().await;
83 info!("Breez SDK disconnected");
84 Ok(())
85 }
86
87 pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
88 parse_input(input, Some(self.external_input_parsers.clone())).await
89 }
90
91 pub async fn get_cross_chain_routes(
98 &self,
99 filter: &CrossChainRouteFilter,
100 ) -> Result<Vec<CrossChainRoutePair>, SdkError> {
101 let mut all_routes = Vec::new();
102 for svc in self.cross_chain_context.values() {
103 match svc.get_routes(filter).await {
104 Ok(routes) => all_routes.extend(routes),
105 Err(e) => tracing::warn!("Cross-chain provider route fetch failed: {e}"),
106 }
107 }
108
109 all_routes.retain(|r| crate::cross_chain::is_usd_stable_asset(&r.asset));
111
112 all_routes.sort_by(|a, b| {
113 a.asset
114 .cmp(&b.asset)
115 .then_with(|| a.chain.cmp(&b.chain))
116 .then_with(|| a.provider.cmp(&b.provider))
117 });
118 Ok(all_routes)
119 }
120
121 #[allow(unused_variables)]
123 pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
124 self.runtime.get_info(self, request).await
125 }
126
127 pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
130 let currencies = self
131 .fiat_service
132 .fetch_fiat_currencies()
133 .await?
134 .into_iter()
135 .map(From::from)
136 .collect();
137 Ok(ListFiatCurrenciesResponse { currencies })
138 }
139
140 pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
142 let rates = self
143 .fiat_service
144 .fetch_fiat_rates()
145 .await?
146 .into_iter()
147 .map(From::from)
148 .collect();
149 Ok(ListFiatRatesResponse { rates })
150 }
151
152 pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
154 Ok(self.chain_service.recommended_fees().await?)
155 }
156
157 pub async fn get_tokens_metadata(
164 &self,
165 request: GetTokensMetadataRequest,
166 ) -> Result<GetTokensMetadataResponse, SdkError> {
167 let metadata = get_tokens_metadata_cached_or_query(
168 &self.spark_wallet,
169 &ObjectCacheRepository::new(self.storage.clone()),
170 &request
171 .token_identifiers
172 .iter()
173 .map(String::as_str)
174 .collect::<Vec<_>>(),
175 )
176 .await?;
177 Ok(GetTokensMetadataResponse {
178 tokens_metadata: metadata,
179 })
180 }
181
182 pub async fn sign_message(
186 &self,
187 request: SignMessageRequest,
188 ) -> Result<SignMessageResponse, SdkError> {
189 use bitcoin::hex::DisplayHex;
190
191 let pubkey = self.spark_wallet.get_identity_public_key().to_string();
192 let signature = self.spark_wallet.sign_message(&request.message).await?;
193 let signature_hex = if request.compact {
194 signature.serialize_compact().to_lower_hex_string()
195 } else {
196 signature.serialize_der().to_lower_hex_string()
197 };
198
199 Ok(SignMessageResponse {
200 pubkey,
201 signature: signature_hex,
202 })
203 }
204
205 pub async fn check_message(
209 &self,
210 request: CheckMessageRequest,
211 ) -> Result<CheckMessageResponse, SdkError> {
212 let pubkey = PublicKey::from_str(&request.pubkey)
213 .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
214 let signature_bytes = hex::decode(&request.signature)
215 .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
216 let signature = Signature::from_der(&signature_bytes)
217 .or_else(|_| Signature::from_compact(&signature_bytes))
218 .map_err(|_| {
219 SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
220 })?;
221
222 let is_valid = self
223 .spark_wallet
224 .verify_message(&request.message, &signature, &pubkey)
225 .await
226 .is_ok();
227 Ok(CheckMessageResponse { is_valid })
228 }
229
230 pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
234 self.maybe_ensure_spark_private_mode_initialized().await?;
236
237 let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
238
239 let stable_balance_active_label = match &self.stable_balance {
240 Some(sb) => sb.get_active_label().await,
241 None => None,
242 };
243
244 Ok(UserSettings {
245 spark_private_mode_enabled: spark_user_settings.private_enabled,
246 stable_balance_active_label,
247 })
248 }
249
250 pub async fn update_user_settings(
254 &self,
255 request: UpdateUserSettingsRequest,
256 ) -> Result<(), SdkError> {
257 if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
258 self.spark_wallet
259 .update_wallet_settings(spark_private_mode_enabled)
260 .await?;
261 }
262
263 if let Some(active_label) = request.stable_balance_active_label {
264 let sb = self
265 .stable_balance
266 .as_ref()
267 .ok_or_else(|| SdkError::Generic("Stable balance is not configured".to_string()))?;
268 let label = if let StableBalanceActiveLabel::Set { label } = active_label {
269 Some(label)
270 } else {
271 None
272 };
273 sb.set_active_token(label).await?;
274 }
275
276 Ok(())
277 }
278
279 pub fn get_token_issuer(&self) -> TokenIssuer {
281 TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
282 }
283
284 pub async fn optimize_leaves(
302 &self,
303 request: OptimizeLeavesRequest,
304 ) -> Result<OptimizeLeavesResponse, SdkError> {
305 let max_rounds = match request.mode {
306 OptimizationMode::Full => None,
307 OptimizationMode::SingleRound => Some(1),
308 };
309 let outcome = self.spark_wallet.optimize_leaves(max_rounds).await?.into();
310 Ok(OptimizeLeavesResponse { outcome })
311 }
312
313 pub async fn register_webhook(
327 &self,
328 request: RegisterWebhookRequest,
329 ) -> Result<RegisterWebhookResponse, SdkError> {
330 let event_types = request.event_types.into_iter().map(Into::into).collect();
331 let webhook_id = self
332 .spark_wallet
333 .register_wallet_webhook(&request.url, &request.secret, event_types)
334 .await
335 .map_err(|e| SdkError::Generic(format!("Failed to register webhook: {e}")))?;
336 Ok(RegisterWebhookResponse { webhook_id })
337 }
338
339 pub async fn unregister_webhook(
348 &self,
349 request: UnregisterWebhookRequest,
350 ) -> Result<(), SdkError> {
351 self.spark_wallet
352 .delete_wallet_webhook(&request.webhook_id)
353 .await
354 .map_err(|e| SdkError::Generic(format!("Failed to unregister webhook: {e}")))?;
355 Ok(())
356 }
357
358 pub async fn list_webhooks(&self) -> Result<Vec<Webhook>, SdkError> {
364 let webhooks = self
365 .spark_wallet
366 .list_wallet_webhooks()
367 .await
368 .map_err(|e| SdkError::Generic(format!("Failed to list webhooks: {e}")))?;
369 Ok(webhooks.into_iter().map(Into::into).collect())
370 }
371
372 pub async fn buy_bitcoin(
380 &self,
381 request: BuyBitcoinRequest,
382 ) -> Result<BuyBitcoinResponse, SdkError> {
383 let url = match request {
384 BuyBitcoinRequest::Moonpay {
385 locked_amount_sat,
386 redirect_url,
387 } => {
388 let address = get_deposit_address(&self.spark_wallet, true).await?;
389 self.buy_bitcoin_provider
390 .buy_bitcoin(address, locked_amount_sat, redirect_url)
391 .await
392 .map_err(|e| {
393 SdkError::Generic(format!("Failed to create buy bitcoin URL: {e}"))
394 })?
395 }
396 BuyBitcoinRequest::CashApp { amount_sats } => {
397 if !matches!(self.config.network, Network::Mainnet) {
398 return Err(SdkError::Generic(
399 "CashApp is only available on mainnet".to_string(),
400 ));
401 }
402 if amount_sats == 0 {
403 return Err(SdkError::Generic(
404 "CashApp requires a non-zero amount".to_string(),
405 ));
406 }
407 let receive_response = self
408 .receive_bolt11_invoice(
409 "Buy Bitcoin via CashApp".to_string(),
410 Some(amount_sats),
411 None,
412 None,
413 )
414 .await?;
415 CashAppProvider::build_url(&receive_response.payment_request)
416 }
417 };
418
419 Ok(BuyBitcoinResponse { url })
420 }
421}