1use bitcoin::secp256k1::{PublicKey, ecdsa::Signature};
2use breez_sdk_common::buy::cashapp::CashAppProvider;
3use spark_wallet::MasterIdentityPublicKeyUpdate;
4use std::str::FromStr;
5use tracing::{debug, info};
6
7use crate::{
8 BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse,
9 CrossChainRouteFilter, CrossChainRoutePair, GetTokensMetadataRequest,
10 GetTokensMetadataResponse, InputType, ListFiatCurrenciesResponse, ListFiatRatesResponse,
11 Network, OptimizationMode, OptimizeLeavesRequest, OptimizeLeavesResponse,
12 RegisterWebhookRequest, RegisterWebhookResponse, SignMessageRequest, SignMessageResponse,
13 UnregisterWebhookRequest, UpdateUserSettingsRequest, UserSettings, Webhook,
14 chain::RecommendedFees,
15 error::SdkError,
16 events::EventListener,
17 issuer::TokenIssuer,
18 models::{
19 GetInfoRequest, GetInfoResponse, SparkMasterIdentityPublicKey, StableBalanceActiveLabel,
20 },
21 persist::ObjectCacheRepository,
22 utils::token::get_tokens_metadata_cached_or_query,
23};
24
25use super::{BreezSdk, helpers::get_deposit_address, parse_input};
26
27#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
28#[allow(clippy::needless_pass_by_value)]
29impl BreezSdk {
30 pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
45 self.event_emitter.add_external_listener(listener).await
46 }
47
48 pub async fn remove_event_listener(&self, id: &str) -> bool {
58 self.event_emitter.remove_external_listener(id).await
59 }
60
61 pub async fn disconnect(&self) -> Result<(), SdkError> {
73 info!("Disconnecting Breez SDK");
74 self.event_emitter.clear_external_listeners().await;
75 if self.shutdown_sender.send(()).is_err() {
76 debug!("No shutdown receivers; SDK has no background tasks to stop");
82 return Ok(());
83 }
84
85 self.shutdown_sender.closed().await;
86 info!("Breez SDK disconnected");
87 Ok(())
88 }
89
90 pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
91 parse_input(input, Some(self.external_input_parsers.clone())).await
92 }
93
94 pub async fn get_cross_chain_routes(
101 &self,
102 filter: &CrossChainRouteFilter,
103 ) -> Result<Vec<CrossChainRoutePair>, SdkError> {
104 let mut all_routes = Vec::new();
105 for svc in self.cross_chain_context.values() {
106 match svc.get_routes(filter).await {
107 Ok(routes) => all_routes.extend(routes),
108 Err(e) => tracing::warn!("Cross-chain provider route fetch failed: {e}"),
109 }
110 }
111
112 all_routes.retain(|r| crate::cross_chain::is_usd_stable_asset(&r.asset));
114
115 all_routes.sort_by(|a, b| {
116 a.asset
117 .cmp(&b.asset)
118 .then_with(|| a.chain.cmp(&b.chain))
119 .then_with(|| a.provider.cmp(&b.provider))
120 });
121 Ok(all_routes)
122 }
123
124 #[allow(unused_variables)]
126 pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
127 self.runtime.get_info(self, request).await
128 }
129
130 pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
133 let currencies = self
134 .fiat_service
135 .fetch_fiat_currencies()
136 .await?
137 .into_iter()
138 .map(From::from)
139 .collect();
140 Ok(ListFiatCurrenciesResponse { currencies })
141 }
142
143 pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
145 let rates = self
146 .fiat_service
147 .fetch_fiat_rates()
148 .await?
149 .into_iter()
150 .map(From::from)
151 .collect();
152 Ok(ListFiatRatesResponse { rates })
153 }
154
155 pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
157 Ok(self.chain_service.recommended_fees().await?)
158 }
159
160 pub async fn get_tokens_metadata(
167 &self,
168 request: GetTokensMetadataRequest,
169 ) -> Result<GetTokensMetadataResponse, SdkError> {
170 let metadata = get_tokens_metadata_cached_or_query(
171 &self.spark_wallet,
172 &ObjectCacheRepository::new(self.storage.clone()),
173 &request
174 .token_identifiers
175 .iter()
176 .map(String::as_str)
177 .collect::<Vec<_>>(),
178 )
179 .await?;
180 Ok(GetTokensMetadataResponse {
181 tokens_metadata: metadata,
182 })
183 }
184
185 pub async fn sign_message(
189 &self,
190 request: SignMessageRequest,
191 ) -> Result<SignMessageResponse, SdkError> {
192 use bitcoin::hex::DisplayHex;
193
194 let pubkey = self.spark_wallet.get_identity_public_key().to_string();
195 let signature = self.spark_wallet.sign_message(&request.message).await?;
196 let signature_hex = if request.compact {
197 signature.serialize_compact().to_lower_hex_string()
198 } else {
199 signature.serialize_der().to_lower_hex_string()
200 };
201
202 Ok(SignMessageResponse {
203 pubkey,
204 signature: signature_hex,
205 })
206 }
207
208 pub async fn check_message(
212 &self,
213 request: CheckMessageRequest,
214 ) -> Result<CheckMessageResponse, SdkError> {
215 let pubkey = PublicKey::from_str(&request.pubkey)
216 .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
217 let signature_bytes = hex::decode(&request.signature)
218 .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
219 let signature = Signature::from_der(&signature_bytes)
220 .or_else(|_| Signature::from_compact(&signature_bytes))
221 .map_err(|_| {
222 SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
223 })?;
224
225 let is_valid = self
226 .spark_wallet
227 .verify_message(&request.message, &signature, &pubkey)
228 .await
229 .is_ok();
230 Ok(CheckMessageResponse { is_valid })
231 }
232
233 pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
237 self.maybe_ensure_spark_private_mode_initialized().await?;
239
240 let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
241
242 let stable_balance_active_label = match &self.stable_balance {
243 Some(sb) => sb.get_active_label().await,
244 None => None,
245 };
246
247 Ok(UserSettings {
248 spark_private_mode_enabled: spark_user_settings.private_enabled,
249 stable_balance_active_label,
250 spark_master_identity_public_key: spark_user_settings
251 .master_identity_public_key
252 .map(|key| key.to_string()),
253 })
254 }
255
256 pub async fn update_user_settings(
260 &self,
261 request: UpdateUserSettingsRequest,
262 ) -> Result<(), SdkError> {
263 let master_identity_public_key = request
264 .spark_master_identity_public_key
265 .map(|update| match update {
266 SparkMasterIdentityPublicKey::Set { public_key } => {
267 parse_compressed_public_key(&public_key).map(MasterIdentityPublicKeyUpdate::Set)
268 }
269 SparkMasterIdentityPublicKey::Unset => Ok(MasterIdentityPublicKeyUpdate::Clear),
270 })
271 .transpose()?;
272
273 self.spark_wallet
274 .update_wallet_settings(
275 request.spark_private_mode_enabled,
276 master_identity_public_key,
277 )
278 .await?;
279
280 if let Some(active_label) = request.stable_balance_active_label {
281 let sb = self
282 .stable_balance
283 .as_ref()
284 .ok_or_else(|| SdkError::Generic("Stable balance is not configured".to_string()))?;
285 let label = if let StableBalanceActiveLabel::Set { label } = active_label {
286 Some(label)
287 } else {
288 None
289 };
290 sb.set_active_token(label).await?;
291 }
292
293 Ok(())
294 }
295
296 pub fn get_token_issuer(&self) -> TokenIssuer {
298 TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
299 }
300
301 pub async fn optimize_leaves(
319 &self,
320 request: OptimizeLeavesRequest,
321 ) -> Result<OptimizeLeavesResponse, SdkError> {
322 let max_rounds = match request.mode {
323 OptimizationMode::Full => None,
324 OptimizationMode::SingleRound => Some(1),
325 };
326 let outcome = self.spark_wallet.optimize_leaves(max_rounds).await?.into();
327 Ok(OptimizeLeavesResponse { outcome })
328 }
329
330 pub async fn register_webhook(
344 &self,
345 request: RegisterWebhookRequest,
346 ) -> Result<RegisterWebhookResponse, SdkError> {
347 let event_types = request.event_types.into_iter().map(Into::into).collect();
348 let webhook_id = self
349 .spark_wallet
350 .register_wallet_webhook(&request.url, &request.secret, event_types)
351 .await
352 .map_err(|e| SdkError::Generic(format!("Failed to register webhook: {e}")))?;
353 Ok(RegisterWebhookResponse { webhook_id })
354 }
355
356 pub async fn unregister_webhook(
365 &self,
366 request: UnregisterWebhookRequest,
367 ) -> Result<(), SdkError> {
368 self.spark_wallet
369 .delete_wallet_webhook(&request.webhook_id)
370 .await
371 .map_err(|e| SdkError::Generic(format!("Failed to unregister webhook: {e}")))?;
372 Ok(())
373 }
374
375 pub async fn list_webhooks(&self) -> Result<Vec<Webhook>, SdkError> {
381 let webhooks = self
382 .spark_wallet
383 .list_wallet_webhooks()
384 .await
385 .map_err(|e| SdkError::Generic(format!("Failed to list webhooks: {e}")))?;
386 Ok(webhooks.into_iter().map(Into::into).collect())
387 }
388
389 pub async fn buy_bitcoin(
397 &self,
398 request: BuyBitcoinRequest,
399 ) -> Result<BuyBitcoinResponse, SdkError> {
400 let url = match request {
401 BuyBitcoinRequest::Moonpay {
402 locked_amount_sat,
403 redirect_url,
404 } => {
405 let address = get_deposit_address(&self.spark_wallet, true).await?;
406 self.buy_bitcoin_provider
407 .buy_bitcoin(address, locked_amount_sat, redirect_url)
408 .await
409 .map_err(|e| {
410 SdkError::Generic(format!("Failed to create buy bitcoin URL: {e}"))
411 })?
412 }
413 BuyBitcoinRequest::CashApp { amount_sats } => {
414 if !matches!(self.config.network, Network::Mainnet) {
415 return Err(SdkError::Generic(
416 "CashApp is only available on mainnet".to_string(),
417 ));
418 }
419 if amount_sats == 0 {
420 return Err(SdkError::Generic(
421 "CashApp requires a non-zero amount".to_string(),
422 ));
423 }
424 let receive_response = self
425 .receive_bolt11_invoice(
426 "Buy Bitcoin via CashApp".to_string(),
427 Some(amount_sats),
428 None,
429 None,
430 )
431 .await?;
432 CashAppProvider::build_url(&receive_response.payment_request)
433 }
434 };
435
436 Ok(BuyBitcoinResponse { url })
437 }
438}
439
440fn parse_compressed_public_key(hex_encoded: &str) -> Result<PublicKey, SdkError> {
446 let invalid = || SdkError::InvalidInput("Invalid master identity public key".to_string());
447 let bytes: [u8; 33] = hex::decode(hex_encoded)
448 .map_err(|_| invalid())?
449 .try_into()
450 .map_err(|_| invalid())?;
451 PublicKey::from_slice(&bytes).map_err(|_| invalid())
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 const COMPRESSED: &str = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
459 const UNCOMPRESSED: &str = "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179\
460 8483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8";
461
462 #[test]
463 fn parse_compressed_public_key_round_trips() {
464 let key = parse_compressed_public_key(COMPRESSED).unwrap();
465 assert_eq!(key.to_string(), COMPRESSED);
466 }
467
468 #[test]
469 fn parse_compressed_public_key_rejects_invalid() {
470 for input in ["", "not-a-public-key", UNCOMPRESSED, &COMPRESSED[..64]] {
471 assert!(
472 matches!(
473 parse_compressed_public_key(input),
474 Err(SdkError::InvalidInput(_))
475 ),
476 "expected {input} to be rejected"
477 );
478 }
479 }
480}