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