1mod api;
2mod contacts;
3mod deposits;
4mod helpers;
5mod init;
6mod lightning_address;
7mod lightning_sender;
8mod lnurl;
9mod payments;
10mod runtime;
11mod sync;
12mod sync_coordinator;
13
14pub(crate) use lightning_sender::LightningSender;
15pub(crate) use runtime::{RuntimeEvent, SdkRuntime, runtime_from_config};
16pub(crate) use sync_coordinator::SyncCoordinator;
17
18use bitflags::bitflags;
19use breez_sdk_common::{buy::moonpay::MoonpayProvider, fiat::FiatService};
20use platform_utils::HttpClient;
21use platform_utils::tokio;
22use spark_wallet::SparkWallet;
23use std::sync::Arc;
24use tokio::sync::{Mutex, OnceCell, oneshot, watch};
25
26use crate::{
27 BitcoinChainService, ExternalInputParser, InputType, LeafOptimizationConfig, Logger, Network,
28 TokenOptimizationConfig, error::SdkError, events::EventEmitter, lnurl::LnurlServerClient,
29 logger, models::Config, persist::Storage, signer::lnurl_auth::LnurlAuthSignerAdapter,
30 stable_balance::StableBalance, token_conversion::TokenConverter,
31};
32
33#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
34const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
35
36#[cfg(all(target_family = "wasm", target_os = "unknown"))]
37const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
38
39pub(crate) const CLAIM_TX_SIZE_VBYTES: u64 = 99;
40pub(crate) const SYNC_PAGING_LIMIT: u32 = 100;
41
42bitflags! {
43 #[derive(Clone, Debug, PartialEq, Eq)]
44 pub(crate) struct SyncType: u32 {
45 const Wallet = 1 << 0;
46 const WalletState = 1 << 1;
47 const Deposits = 1 << 2;
48 const LnurlMetadata = 1 << 3;
49 const Full = Self::Wallet.0.0
50 | Self::WalletState.0.0
51 | Self::Deposits.0.0
52 | Self::LnurlMetadata.0.0;
53 }
54}
55
56#[derive(Clone, Debug)]
57pub(crate) struct SyncRequest {
58 pub(crate) sync_type: SyncType,
59 #[allow(clippy::type_complexity)]
60 pub(crate) reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
61 pub(crate) force: bool,
64}
65
66impl SyncRequest {
67 pub(crate) async fn reply(&self, error: Option<SdkError>) {
68 if let Some(reply) = self.reply.lock().await.take() {
69 let _ = match error {
70 Some(e) => reply.send(Err(e)),
71 None => reply.send(Ok(())),
72 };
73 }
74 }
75}
76
77#[derive(Clone)]
80#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
81pub struct BreezSdk {
82 pub(crate) config: Config,
83 pub(crate) spark_wallet: Arc<SparkWallet>,
84 pub(crate) storage: Arc<dyn Storage>,
85 pub(crate) chain_service: Arc<dyn BitcoinChainService>,
86 pub(crate) fiat_service: Arc<dyn FiatService>,
87 pub(crate) lnurl_client: Arc<dyn HttpClient>,
88 pub(crate) lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
89 pub(crate) lnurl_auth_signer: Option<Arc<LnurlAuthSignerAdapter>>,
90 pub(crate) event_emitter: Arc<EventEmitter>,
91 pub(crate) shutdown_sender: watch::Sender<()>,
92 pub(crate) runtime: SdkRuntime,
93 pub(crate) sync_coordinator: SyncCoordinator,
95 pub(crate) initial_synced_watcher: watch::Receiver<bool>,
96 pub(crate) external_input_parsers: Vec<ExternalInputParser>,
97 pub(crate) spark_private_mode_initialized: Arc<OnceCell<()>>,
98 pub(crate) token_converter: Arc<dyn TokenConverter>,
99 pub(crate) stable_balance: Option<Arc<StableBalance>>,
100 pub(crate) buy_bitcoin_provider: Arc<MoonpayProvider>,
101 pub(crate) cross_chain_context: crate::cross_chain::CrossChainContext,
102 #[allow(dead_code)]
106 pub(crate) lightning_sender: Arc<LightningSender>,
107}
108
109pub(crate) struct BreezSdkParams {
110 pub config: Config,
111 pub storage: Arc<dyn Storage>,
112 pub chain_service: Arc<dyn BitcoinChainService>,
113 pub fiat_service: Arc<dyn FiatService>,
114 pub lnurl_client: Arc<dyn HttpClient>,
115 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
116 pub lnurl_auth_signer: Option<Arc<LnurlAuthSignerAdapter>>,
117 pub shutdown_sender: watch::Sender<()>,
118 pub runtime: SdkRuntime,
119 pub spark_wallet: Arc<SparkWallet>,
120 pub event_emitter: Arc<EventEmitter>,
121 pub buy_bitcoin_provider: Arc<MoonpayProvider>,
122 pub token_converter: Arc<dyn TokenConverter>,
123 pub stable_balance: Option<Arc<StableBalance>>,
124 pub sync_coordinator: SyncCoordinator,
125 pub cross_chain_context: crate::cross_chain::CrossChainContext,
126 pub lightning_sender: Arc<LightningSender>,
127}
128
129pub async fn parse_input(
130 input: &str,
131 external_input_parsers: Option<Vec<ExternalInputParser>>,
132) -> Result<InputType, SdkError> {
133 Ok(breez_sdk_common::input::parse(
134 input,
135 external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
136 )
137 .await?
138 .into())
139}
140
141#[allow(clippy::needless_pass_by_value)]
142#[cfg_attr(feature = "uniffi", uniffi::export)]
143pub fn init_logging(
144 log_dir: Option<String>,
145 app_logger: Option<Box<dyn Logger>>,
146 log_filter: Option<String>,
147) -> Result<(), SdkError> {
148 logger::init_logging(log_dir.as_deref(), app_logger, log_filter.as_deref())
149}
150
151#[cfg(feature = "sqlite")]
161#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
162pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
163 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
164 .with_default_storage(request.storage_dir);
165 let sdk = builder.build().await?;
166 Ok(sdk)
167}
168
169#[cfg(feature = "sqlite")]
182#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
183pub async fn connect_with_signer(
184 request: crate::ConnectWithSignerRequest,
185) -> Result<BreezSdk, SdkError> {
186 let builder = super::sdk_builder::SdkBuilder::new_with_signer(
187 request.config,
188 request.breez_signer,
189 request.spark_signer,
190 )
191 .with_default_storage(request.storage_dir);
192 let sdk = builder.build().await?;
193 Ok(sdk)
194}
195
196#[cfg(feature = "sqlite")]
211#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
212pub async fn connect_with_signing_only_signer(
213 request: crate::ConnectWithSigningOnlySignerRequest,
214) -> Result<BreezSdk, SdkError> {
215 let builder = super::sdk_builder::SdkBuilder::new_with_signing_only_signer(
216 request.config,
217 request.breez_signer,
218 request.spark_signer,
219 )
220 .with_default_storage(request.storage_dir);
221 let sdk = builder.build().await?;
222 Ok(sdk)
223}
224
225#[cfg_attr(feature = "uniffi", uniffi::export)]
226pub fn default_config(network: Network) -> Config {
227 let lnurl_domain = match network {
228 Network::Mainnet => Some("breez.tips".to_string()),
229 Network::Regtest => None,
230 };
231 Config {
232 api_key: None,
233 network,
234 sync_interval_secs: 60, max_deposit_claim_fee: Some(crate::MaxFee::Rate { sat_per_vbyte: 1 }),
236 lnurl_domain,
237 prefer_spark_over_lightning: false,
238 external_input_parsers: None,
239 use_default_external_input_parsers: true,
240 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
241 private_enabled_default: true,
242 leaf_optimization_config: LeafOptimizationConfig {
243 auto_enabled: true,
244 multiplicity: 1,
245 },
246 token_optimization_config: TokenOptimizationConfig {
247 auto_enabled: true,
248 target_output_count: 5,
249 min_outputs_threshold: 50,
250 },
251 stable_balance_config: None,
252 max_concurrent_claims: 4,
253 spark_config: Some(default_spark_config(network)),
254 background_tasks_enabled: true,
255 cross_chain_config: None,
256 }
257}
258
259#[cfg_attr(feature = "uniffi", uniffi::export)]
293pub fn default_server_config(network: Network) -> Config {
294 let mut config = default_config(network);
295 config.background_tasks_enabled = false;
296 config.real_time_sync_server_url = None;
297 config.leaf_optimization_config.auto_enabled = false;
298 config.token_optimization_config.auto_enabled = false;
299 config.cross_chain_config = None;
300 config
301}
302
303fn default_spark_config(network: Network) -> crate::models::SparkConfig {
309 use crate::models::{SparkSigningOperator, SparkSspConfig};
310
311 let wallet_config = spark_wallet::SparkWalletConfig::default_config(network.into());
312
313 let coordinator_identifier = hex::encode(
314 wallet_config
315 .operator_pool
316 .get_coordinator()
317 .identifier
318 .serialize(),
319 );
320
321 let signing_operators = wallet_config
322 .operator_pool
323 .get_all_operators()
324 .map(|op| SparkSigningOperator {
325 id: u32::try_from(op.id).expect("operator id fits in u32"),
326 identifier: hex::encode(op.identifier.serialize()),
327 address: op.address.clone(),
328 identity_public_key: hex::encode(op.identity_public_key.serialize()),
329 ca_cert_pem: op
330 .ca_cert
331 .as_ref()
332 .and_then(|b| String::from_utf8(b.clone()).ok()),
333 })
334 .collect();
335
336 let ssp = &wallet_config.service_provider_config;
337
338 crate::models::SparkConfig {
339 coordinator_identifier,
340 threshold: wallet_config.split_secret_threshold,
341 signing_operators,
342 ssp_config: SparkSspConfig {
343 base_url: ssp.base_url.clone(),
344 identity_public_key: hex::encode(ssp.identity_public_key.serialize()),
345 schema_endpoint: ssp.schema_endpoint.clone(),
346 },
347 expected_withdraw_bond_sats: wallet_config.tokens_config.expected_withdraw_bond_sats,
348 expected_withdraw_relative_block_locktime: wallet_config
349 .tokens_config
350 .expected_withdraw_relative_block_locktime,
351 max_token_transaction_inputs: None,
352 }
353}
354
355#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
358pub struct ExternalSigners {
359 pub breez_signer: Arc<dyn crate::signer::ExternalBreezSigner>,
362 pub spark_signer: Arc<dyn crate::signer::ExternalSparkSigner>,
364}
365
366#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
370pub struct SigningOnlyExternalSigners {
371 pub breez_signer: Arc<dyn crate::signer::ExternalSigningSigner>,
373 pub spark_signer: Arc<dyn crate::signer::ExternalSparkSigner>,
375}
376
377#[cfg_attr(feature = "uniffi", uniffi::export)]
392pub fn default_external_signers(
393 mnemonic: String,
394 passphrase: Option<String>,
395 network: Network,
396 account_number: Option<u32>,
397) -> Result<ExternalSigners, SdkError> {
398 use crate::signer::{DefaultExternalSigner, DefaultExternalSparkSigner};
399
400 let breez_signer = DefaultExternalSigner::new(
401 mnemonic.clone(),
402 passphrase.clone(),
403 network,
404 account_number,
405 )?;
406 let spark_signer =
407 DefaultExternalSparkSigner::new(mnemonic, passphrase, network, account_number)?;
408
409 Ok(ExternalSigners {
410 breez_signer: Arc::new(breez_signer),
411 spark_signer: Arc::new(spark_signer),
412 })
413}
414
415#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
420pub async fn get_spark_status() -> Result<crate::SparkStatus, SdkError> {
421 use chrono::DateTime;
422 use platform_utils::DefaultHttpClient;
423
424 #[derive(serde::Deserialize)]
425 struct StatusApiResponse {
426 services: Vec<StatusApiService>,
427 #[serde(rename = "lastUpdated")]
428 last_updated: String,
429 }
430
431 #[derive(serde::Deserialize)]
432 struct StatusApiService {
433 name: String,
434 status: String,
435 }
436
437 fn parse_service_status(s: &str) -> crate::ServiceStatus {
438 match s {
439 "operational" => crate::ServiceStatus::Operational,
440 "degraded" => crate::ServiceStatus::Degraded,
441 "partial" => crate::ServiceStatus::Partial,
442 "major" => crate::ServiceStatus::Major,
443 _ => {
444 tracing::warn!("Unknown service status: {s}");
445 crate::ServiceStatus::Unknown
446 }
447 }
448 }
449
450 let http_client = DefaultHttpClient::default();
451
452 let response = http_client
453 .get("https://spark.money/api/v1/status".to_string(), None)
454 .await
455 .map_err(|e| SdkError::NetworkError(e.to_string()))?;
456
457 let api_response: StatusApiResponse = response
458 .json()
459 .map_err(|e| SdkError::Generic(format!("Failed to parse status response: {e}")))?;
460
461 let status = api_response
462 .services
463 .iter()
464 .filter(|s| s.name == "Spark Operators" || s.name == "SSP")
465 .map(|s| parse_service_status(&s.status))
466 .max()
467 .unwrap_or(crate::ServiceStatus::Unknown);
468
469 let last_updated = DateTime::parse_from_rfc3339(&api_response.last_updated)
470 .map(|dt| dt.timestamp().cast_unsigned())
471 .map_err(|e| SdkError::Generic(format!("Failed to parse lastUpdated timestamp: {e}")))?;
472
473 Ok(crate::SparkStatus {
474 status,
475 last_updated,
476 })
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn default_server_config_disables_background_tasks() {
485 for network in [Network::Mainnet, Network::Regtest] {
486 let cfg = default_server_config(network);
487 assert!(!cfg.background_tasks_enabled);
488 assert!(cfg.real_time_sync_server_url.is_none());
489 assert!(!cfg.leaf_optimization_config.auto_enabled);
490 assert!(!cfg.token_optimization_config.auto_enabled);
491 }
492 }
493
494 #[test]
495 fn default_config_enables_background_tasks() {
496 assert!(default_config(Network::Mainnet).background_tasks_enabled);
497 assert!(default_config(Network::Regtest).background_tasks_enabled);
498 }
499}