1mod api;
2mod contacts;
3mod deposits;
4mod helpers;
5mod init;
6mod lightning_address;
7mod lnurl;
8mod payments;
9mod runtime;
10mod sync;
11mod sync_coordinator;
12
13pub(crate) use runtime::{RuntimeEvent, SdkRuntime, runtime_from_config};
14pub(crate) use sync_coordinator::SyncCoordinator;
15
16use bitflags::bitflags;
17use breez_sdk_common::{buy::moonpay::MoonpayProvider, fiat::FiatService};
18use platform_utils::HttpClient;
19use platform_utils::tokio;
20use spark_wallet::SparkWallet;
21use std::sync::Arc;
22use tokio::sync::{Mutex, OnceCell, oneshot, watch};
23
24use crate::{
25 BitcoinChainService, ExternalInputParser, InputType, LeafOptimizationConfig, Logger, Network,
26 TokenOptimizationConfig, error::SdkError, events::EventEmitter, lnurl::LnurlServerClient,
27 logger, models::Config, persist::Storage, signer::lnurl_auth::LnurlAuthSignerAdapter,
28 stable_balance::StableBalance, token_conversion::TokenConverter,
29};
30
31#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
32const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
33
34#[cfg(all(target_family = "wasm", target_os = "unknown"))]
35const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
36
37pub(crate) const CLAIM_TX_SIZE_VBYTES: u64 = 99;
38pub(crate) const SYNC_PAGING_LIMIT: u32 = 100;
39
40bitflags! {
41 #[derive(Clone, Debug, PartialEq, Eq)]
42 pub(crate) struct SyncType: u32 {
43 const Wallet = 1 << 0;
44 const WalletState = 1 << 1;
45 const Deposits = 1 << 2;
46 const LnurlMetadata = 1 << 3;
47 const Full = Self::Wallet.0.0
48 | Self::WalletState.0.0
49 | Self::Deposits.0.0
50 | Self::LnurlMetadata.0.0;
51 }
52}
53
54#[derive(Clone, Debug)]
55pub(crate) struct SyncRequest {
56 pub(crate) sync_type: SyncType,
57 #[allow(clippy::type_complexity)]
58 pub(crate) reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
59 pub(crate) force: bool,
62}
63
64impl SyncRequest {
65 pub(crate) async fn reply(&self, error: Option<SdkError>) {
66 if let Some(reply) = self.reply.lock().await.take() {
67 let _ = match error {
68 Some(e) => reply.send(Err(e)),
69 None => reply.send(Ok(())),
70 };
71 }
72 }
73}
74
75#[derive(Clone)]
78#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
79pub struct BreezSdk {
80 pub(crate) config: Config,
81 pub(crate) spark_wallet: Arc<SparkWallet>,
82 pub(crate) storage: Arc<dyn Storage>,
83 pub(crate) chain_service: Arc<dyn BitcoinChainService>,
84 pub(crate) fiat_service: Arc<dyn FiatService>,
85 pub(crate) lnurl_client: Arc<dyn HttpClient>,
86 pub(crate) lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
87 pub(crate) lnurl_auth_signer: Arc<LnurlAuthSignerAdapter>,
88 pub(crate) event_emitter: Arc<EventEmitter>,
89 pub(crate) shutdown_sender: watch::Sender<()>,
90 pub(crate) runtime: SdkRuntime,
91 pub(crate) sync_coordinator: SyncCoordinator,
93 pub(crate) initial_synced_watcher: watch::Receiver<bool>,
94 pub(crate) external_input_parsers: Vec<ExternalInputParser>,
95 pub(crate) spark_private_mode_initialized: Arc<OnceCell<()>>,
96 pub(crate) token_converter: Arc<dyn TokenConverter>,
97 pub(crate) stable_balance: Option<Arc<StableBalance>>,
98 pub(crate) buy_bitcoin_provider: Arc<MoonpayProvider>,
99}
100
101pub(crate) struct BreezSdkParams {
102 pub config: Config,
103 pub storage: Arc<dyn Storage>,
104 pub chain_service: Arc<dyn BitcoinChainService>,
105 pub fiat_service: Arc<dyn FiatService>,
106 pub lnurl_client: Arc<dyn HttpClient>,
107 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
108 pub lnurl_auth_signer: Arc<LnurlAuthSignerAdapter>,
109 pub shutdown_sender: watch::Sender<()>,
110 pub runtime: SdkRuntime,
111 pub spark_wallet: Arc<SparkWallet>,
112 pub event_emitter: Arc<EventEmitter>,
113 pub buy_bitcoin_provider: Arc<MoonpayProvider>,
114 pub token_converter: Arc<dyn TokenConverter>,
115 pub stable_balance: Option<Arc<StableBalance>>,
116 pub sync_coordinator: SyncCoordinator,
117}
118
119pub async fn parse_input(
120 input: &str,
121 external_input_parsers: Option<Vec<ExternalInputParser>>,
122) -> Result<InputType, SdkError> {
123 Ok(breez_sdk_common::input::parse(
124 input,
125 external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
126 )
127 .await?
128 .into())
129}
130
131#[cfg_attr(feature = "uniffi", uniffi::export)]
132pub fn init_logging(
133 log_dir: Option<String>,
134 app_logger: Option<Box<dyn Logger>>,
135 log_filter: Option<String>,
136) -> Result<(), SdkError> {
137 logger::init_logging(log_dir, app_logger, log_filter)
138}
139
140#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
150#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
151pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
152 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
153 .with_default_storage(request.storage_dir);
154 let sdk = builder.build().await?;
155 Ok(sdk)
156}
157
158#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
171#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
172pub async fn connect_with_signer(
173 request: crate::ConnectWithSignerRequest,
174) -> Result<BreezSdk, SdkError> {
175 let builder = super::sdk_builder::SdkBuilder::new_with_signer(request.config, request.signer)
176 .with_default_storage(request.storage_dir);
177 let sdk = builder.build().await?;
178 Ok(sdk)
179}
180
181#[cfg_attr(feature = "uniffi", uniffi::export)]
182pub fn default_config(network: Network) -> Config {
183 let lnurl_domain = match network {
184 Network::Mainnet => Some("breez.tips".to_string()),
185 Network::Regtest => None,
186 };
187 Config {
188 api_key: None,
189 network,
190 sync_interval_secs: 60, max_deposit_claim_fee: Some(crate::MaxFee::Rate { sat_per_vbyte: 1 }),
192 lnurl_domain,
193 prefer_spark_over_lightning: false,
194 external_input_parsers: None,
195 use_default_external_input_parsers: true,
196 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
197 private_enabled_default: true,
198 leaf_optimization_config: LeafOptimizationConfig {
199 auto_enabled: true,
200 multiplicity: 1,
201 },
202 token_optimization_config: TokenOptimizationConfig {
203 auto_enabled: true,
204 target_output_count: 5,
205 min_outputs_threshold: 50,
206 },
207 stable_balance_config: None,
208 max_concurrent_claims: 4,
209 spark_config: Some(default_spark_config(network)),
210 background_tasks_enabled: true,
211 }
212}
213
214#[cfg_attr(feature = "uniffi", uniffi::export)]
248pub fn default_server_config(network: Network) -> Config {
249 let mut config = default_config(network);
250 config.background_tasks_enabled = false;
251 config.real_time_sync_server_url = None;
252 config.leaf_optimization_config.auto_enabled = false;
253 config.token_optimization_config.auto_enabled = false;
254 config
255}
256
257fn default_spark_config(network: Network) -> crate::models::SparkConfig {
263 use crate::models::{SparkSigningOperator, SparkSspConfig};
264
265 let wallet_config = spark_wallet::SparkWalletConfig::default_config(network.into());
266
267 let coordinator_identifier = hex::encode(
268 wallet_config
269 .operator_pool
270 .get_coordinator()
271 .identifier
272 .serialize(),
273 );
274
275 let signing_operators = wallet_config
276 .operator_pool
277 .get_all_operators()
278 .map(|op| SparkSigningOperator {
279 id: u32::try_from(op.id).expect("operator id fits in u32"),
280 identifier: hex::encode(op.identifier.serialize()),
281 address: op.address.clone(),
282 identity_public_key: hex::encode(op.identity_public_key.serialize()),
283 })
284 .collect();
285
286 let ssp = &wallet_config.service_provider_config;
287
288 crate::models::SparkConfig {
289 coordinator_identifier,
290 threshold: wallet_config.split_secret_threshold,
291 signing_operators,
292 ssp_config: SparkSspConfig {
293 base_url: ssp.base_url.clone(),
294 identity_public_key: hex::encode(ssp.identity_public_key.serialize()),
295 schema_endpoint: ssp.schema_endpoint.clone(),
296 },
297 expected_withdraw_bond_sats: wallet_config.tokens_config.expected_withdraw_bond_sats,
298 expected_withdraw_relative_block_locktime: wallet_config
299 .tokens_config
300 .expected_withdraw_relative_block_locktime,
301 }
302}
303
304#[cfg_attr(feature = "uniffi", uniffi::export)]
320pub fn default_external_signer(
321 mnemonic: String,
322 passphrase: Option<String>,
323 network: Network,
324 key_set_config: Option<crate::models::KeySetConfig>,
325) -> Result<Arc<dyn crate::signer::ExternalSigner>, SdkError> {
326 use crate::signer::DefaultExternalSigner;
327
328 let config = key_set_config.unwrap_or_default();
329 let signer = DefaultExternalSigner::new(
330 mnemonic,
331 passphrase,
332 network,
333 config.key_set_type,
334 config.use_address_index,
335 config.account_number,
336 )?;
337
338 Ok(Arc::new(signer))
339}
340
341#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
346pub async fn get_spark_status() -> Result<crate::SparkStatus, SdkError> {
347 use chrono::DateTime;
348 use platform_utils::DefaultHttpClient;
349
350 #[derive(serde::Deserialize)]
351 struct StatusApiResponse {
352 services: Vec<StatusApiService>,
353 #[serde(rename = "lastUpdated")]
354 last_updated: String,
355 }
356
357 #[derive(serde::Deserialize)]
358 struct StatusApiService {
359 name: String,
360 status: String,
361 }
362
363 fn parse_service_status(s: &str) -> crate::ServiceStatus {
364 match s {
365 "operational" => crate::ServiceStatus::Operational,
366 "degraded" => crate::ServiceStatus::Degraded,
367 "partial" => crate::ServiceStatus::Partial,
368 "major" => crate::ServiceStatus::Major,
369 _ => {
370 tracing::warn!("Unknown service status: {s}");
371 crate::ServiceStatus::Unknown
372 }
373 }
374 }
375
376 let http_client = DefaultHttpClient::default();
377
378 let response = http_client
379 .get("https://spark.money/api/v1/status".to_string(), None)
380 .await
381 .map_err(|e| SdkError::NetworkError(e.to_string()))?;
382
383 let api_response: StatusApiResponse = response
384 .json()
385 .map_err(|e| SdkError::Generic(format!("Failed to parse status response: {e}")))?;
386
387 let status = api_response
388 .services
389 .iter()
390 .filter(|s| s.name == "Spark Operators" || s.name == "SSP")
391 .map(|s| parse_service_status(&s.status))
392 .max()
393 .unwrap_or(crate::ServiceStatus::Unknown);
394
395 let last_updated = DateTime::parse_from_rfc3339(&api_response.last_updated)
396 .map(|dt| dt.timestamp().cast_unsigned())
397 .map_err(|e| SdkError::Generic(format!("Failed to parse lastUpdated timestamp: {e}")))?;
398
399 Ok(crate::SparkStatus {
400 status,
401 last_updated,
402 })
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn default_server_config_disables_background_tasks() {
411 for network in [Network::Mainnet, Network::Regtest] {
412 let cfg = default_server_config(network);
413 assert!(!cfg.background_tasks_enabled);
414 assert!(cfg.real_time_sync_server_url.is_none());
415 assert!(!cfg.leaf_optimization_config.auto_enabled);
416 assert!(!cfg.token_optimization_config.auto_enabled);
417 }
418 }
419
420 #[test]
421 fn default_config_enables_background_tasks() {
422 assert!(default_config(Network::Mainnet).background_tasks_enabled);
423 assert!(default_config(Network::Regtest).background_tasks_enabled);
424 }
425}