1mod api;
2mod contacts;
3mod deposits;
4pub(crate) mod exit_chain_downloader;
5mod helpers;
6mod init;
7mod lightning_address;
8mod lightning_sender;
9mod lnurl;
10mod payments;
11mod runtime;
12mod sync;
13mod sync_coordinator;
14mod unilateral_exit;
15mod unilateral_exit_backup;
16
17pub(crate) use lightning_sender::{LightningSender, PendingLightningSend};
18pub(crate) use runtime::{RuntimeEvent, SdkRuntime, runtime_from_config};
19pub(crate) use sync_coordinator::SyncCoordinator;
20
21use bitflags::bitflags;
22use breez_sdk_common::{buy::moonpay::MoonpayProvider, fiat::FiatService};
23use platform_utils::HttpClient;
24use platform_utils::tokio;
25use spark_wallet::SparkWallet;
26use std::sync::Arc;
27use tokio::sync::{Mutex, OnceCell, oneshot, watch};
28
29use crate::{
30 BitcoinChainService, LeafOptimizationConfig, Logger, Network, TokenOptimizationConfig,
31 error::SdkError, events::EventEmitter, lnurl::LnurlServerClient, logger, models::Config,
32 persist::Storage, signer::lnurl_auth::LnurlAuthSignerAdapter, stable_balance::StableBalance,
33 token_conversion::TokenConverter,
34};
35
36#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
37const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
38
39#[cfg(all(target_family = "wasm", target_os = "unknown"))]
40const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
41
42pub(crate) const CLAIM_TX_SIZE_VBYTES: u64 = 99;
43pub(crate) const SYNC_PAGING_LIMIT: u32 = 100;
44
45bitflags! {
46 #[derive(Clone, Debug, PartialEq, Eq)]
47 pub(crate) struct SyncType: u32 {
48 const Wallet = 1 << 0;
49 const WalletState = 1 << 1;
50 const Deposits = 1 << 2;
51 const LnurlMetadata = 1 << 3;
52 const Full = Self::Wallet.0.0
53 | Self::WalletState.0.0
54 | Self::Deposits.0.0
55 | Self::LnurlMetadata.0.0;
56 }
57}
58
59#[derive(Clone, Debug)]
60pub(crate) struct SyncRequest {
61 pub(crate) sync_type: SyncType,
62 #[allow(clippy::type_complexity)]
63 pub(crate) reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
64 pub(crate) force: bool,
67}
68
69impl SyncRequest {
70 pub(crate) async fn reply(&self, error: Option<SdkError>) {
71 if let Some(reply) = self.reply.lock().await.take() {
72 let _ = match error {
73 Some(e) => reply.send(Err(e)),
74 None => reply.send(Ok(())),
75 };
76 }
77 }
78}
79
80#[derive(Clone)]
83#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
84pub struct BreezSdk {
85 pub(crate) config: Config,
86 pub(crate) spark_wallet: Arc<SparkWallet>,
87 pub(crate) storage: Arc<dyn Storage>,
88 pub(crate) chain_service: Arc<dyn BitcoinChainService>,
89 pub(crate) fiat_service: Arc<dyn FiatService>,
90 pub(crate) lnurl_client: Arc<dyn HttpClient>,
91 pub(crate) lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
92 pub(crate) lnurl_auth_signer: Option<Arc<LnurlAuthSignerAdapter>>,
93 pub(crate) event_emitter: Arc<EventEmitter>,
94 pub(crate) shutdown_sender: watch::Sender<()>,
95 pub(crate) exit_chain_trigger: exit_chain_downloader::ExitChainTrigger,
98 pub(crate) runtime: SdkRuntime,
99 pub(crate) sync_coordinator: SyncCoordinator,
101 pub(crate) claim_guards: deposits::ClaimGuards,
104 pub(crate) initial_synced_watcher: watch::Receiver<bool>,
105 pub(crate) input_parser: Arc<SdkInputParser>,
109 pub(crate) spark_private_mode_initialized: Arc<OnceCell<()>>,
110 pub(crate) token_converter: Arc<dyn TokenConverter>,
111 pub(crate) stable_balance: Option<Arc<StableBalance>>,
112 pub(crate) buy_bitcoin_provider: Arc<MoonpayProvider>,
113 pub(crate) cross_chain_context: crate::cross_chain::CrossChainContext,
114 #[allow(dead_code)]
118 pub(crate) lightning_sender: Arc<LightningSender>,
119}
120
121pub(crate) type SdkInputParser =
123 breez_sdk_common::input::InputParser<Arc<dyn HttpClient>, breez_sdk_common::dns::Resolver>;
124
125pub(crate) struct BreezSdkParams {
126 pub config: Config,
127 pub input_parser: Arc<SdkInputParser>,
128 pub storage: Arc<dyn Storage>,
129 pub chain_service: Arc<dyn BitcoinChainService>,
130 pub fiat_service: Arc<dyn FiatService>,
131 pub lnurl_client: Arc<dyn HttpClient>,
132 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
133 pub lnurl_auth_signer: Option<Arc<LnurlAuthSignerAdapter>>,
134 pub shutdown_sender: watch::Sender<()>,
135 pub runtime: SdkRuntime,
136 pub spark_wallet: Arc<SparkWallet>,
137 pub event_emitter: Arc<EventEmitter>,
138 pub buy_bitcoin_provider: Arc<MoonpayProvider>,
139 pub token_converter: Arc<dyn TokenConverter>,
140 pub stable_balance: Option<Arc<StableBalance>>,
141 pub sync_coordinator: SyncCoordinator,
142 pub cross_chain_context: crate::cross_chain::CrossChainContext,
143 pub lightning_sender: Arc<LightningSender>,
144}
145
146#[allow(clippy::needless_pass_by_value)]
147#[cfg_attr(feature = "uniffi", uniffi::export)]
148pub fn init_logging(
149 log_dir: Option<String>,
150 app_logger: Option<Box<dyn Logger>>,
151 log_filter: Option<String>,
152) -> Result<(), SdkError> {
153 logger::init_logging(log_dir.as_deref(), app_logger, log_filter.as_deref())
154}
155
156#[cfg(feature = "sqlite")]
166#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
167pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
168 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
169 .with_default_storage(request.storage_dir);
170 let sdk = builder.build().await?;
171 Ok(sdk)
172}
173
174#[cfg(feature = "sqlite")]
187#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
188pub async fn connect_with_signer(
189 request: crate::ConnectWithSignerRequest,
190) -> Result<BreezSdk, SdkError> {
191 let builder = super::sdk_builder::SdkBuilder::new_with_signer(
192 request.config,
193 request.breez_signer,
194 request.spark_signer,
195 )
196 .with_default_storage(request.storage_dir);
197 let sdk = builder.build().await?;
198 Ok(sdk)
199}
200
201#[cfg(feature = "sqlite")]
216#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
217pub async fn connect_with_signing_only_signer(
218 request: crate::ConnectWithSigningOnlySignerRequest,
219) -> Result<BreezSdk, SdkError> {
220 let builder = super::sdk_builder::SdkBuilder::new_with_signing_only_signer(
221 request.config,
222 request.breez_signer,
223 request.spark_signer,
224 )
225 .with_default_storage(request.storage_dir);
226 let sdk = builder.build().await?;
227 Ok(sdk)
228}
229
230#[cfg_attr(feature = "uniffi", uniffi::export)]
231pub fn default_config(network: Network) -> Config {
232 let lnurl_domain = match network {
233 Network::Mainnet => Some("breez.tips".to_string()),
234 Network::Signet | Network::Regtest => None,
235 };
236 Config {
237 api_key: None,
238 network,
239 sync_interval_secs: 60, max_deposit_claim_fee: Some(crate::MaxFee::Rate { sat_per_vbyte: 1 }),
241 lnurl_domain,
242 prefer_spark_over_lightning: false,
243 exit_chain_auto_fetch_enabled: true,
244 external_input_parsers: None,
245 use_default_external_input_parsers: true,
246 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
247 private_enabled_default: true,
248 leaf_optimization_config: LeafOptimizationConfig {
249 auto_enabled: true,
250 multiplicity: 1,
251 },
252 token_optimization_config: TokenOptimizationConfig {
253 auto_enabled: true,
254 target_output_count: 5,
255 min_outputs_threshold: 50,
256 },
257 stable_balance_config: None,
258 max_concurrent_claims: 4,
259 spark_config: match network {
260 Network::Mainnet | Network::Regtest => Some(default_spark_config(network)),
261 Network::Signet => None,
262 },
263 background_tasks_enabled: true,
264 proxy: None,
265 cross_chain_config: None,
266 }
267}
268
269#[cfg_attr(feature = "uniffi", uniffi::export)]
303pub fn default_server_config(network: Network) -> Config {
304 let mut config = default_config(network);
305 config.background_tasks_enabled = false;
306 config.real_time_sync_server_url = None;
307 config.leaf_optimization_config.auto_enabled = false;
308 config.token_optimization_config.auto_enabled = false;
309 config.cross_chain_config = None;
310 config
311}
312
313fn default_spark_config(network: Network) -> crate::models::SparkConfig {
319 use crate::models::{SparkSigningOperator, SparkSspConfig};
320
321 let wallet_config = spark_wallet::SparkWalletConfig::default_config(network.into());
322
323 let coordinator_identifier = hex::encode(
324 wallet_config
325 .operator_pool
326 .get_coordinator()
327 .identifier
328 .serialize(),
329 );
330
331 let signing_operators = wallet_config
332 .operator_pool
333 .get_all_operators()
334 .map(|op| SparkSigningOperator {
335 id: u32::try_from(op.id).expect("operator id fits in u32"),
336 identifier: hex::encode(op.identifier.serialize()),
337 address: op.address.clone(),
338 identity_public_key: hex::encode(op.identity_public_key.serialize()),
339 ca_cert_pem: op
340 .ca_cert
341 .as_ref()
342 .and_then(|b| String::from_utf8(b.clone()).ok()),
343 })
344 .collect();
345
346 let ssp = &wallet_config.service_provider_config;
347
348 crate::models::SparkConfig {
349 coordinator_identifier,
350 threshold: wallet_config.split_secret_threshold,
351 signing_operators,
352 ssp_config: SparkSspConfig {
353 base_url: ssp.base_url.clone(),
354 identity_public_key: hex::encode(ssp.identity_public_key.serialize()),
355 schema_endpoint: ssp.schema_endpoint.clone(),
356 },
357 expected_withdraw_bond_sats: wallet_config.tokens_config.expected_withdraw_bond_sats,
358 expected_withdraw_relative_block_locktime: wallet_config
359 .tokens_config
360 .expected_withdraw_relative_block_locktime,
361 max_token_transaction_inputs: None,
362 }
363}
364
365#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
368pub struct ExternalSigners {
369 pub breez_signer: Arc<dyn crate::signer::ExternalBreezSigner>,
372 pub spark_signer: Arc<dyn crate::signer::ExternalSparkSigner>,
374}
375
376#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
380pub struct SigningOnlyExternalSigners {
381 pub breez_signer: Arc<dyn crate::signer::ExternalSigningSigner>,
383 pub spark_signer: Arc<dyn crate::signer::ExternalSparkSigner>,
385}
386
387#[cfg_attr(feature = "uniffi", uniffi::export)]
402pub fn default_external_signers(
403 mnemonic: String,
404 passphrase: Option<String>,
405 network: Network,
406 account_number: Option<u32>,
407) -> Result<ExternalSigners, SdkError> {
408 use crate::signer::{DefaultExternalSigner, DefaultExternalSparkSigner};
409
410 let breez_signer = DefaultExternalSigner::new(
411 mnemonic.clone(),
412 passphrase.clone(),
413 network,
414 account_number,
415 )?;
416 let spark_signer =
417 DefaultExternalSparkSigner::new(mnemonic, passphrase, network, account_number)?;
418
419 Ok(ExternalSigners {
420 breez_signer: Arc::new(breez_signer),
421 spark_signer: Arc::new(spark_signer),
422 })
423}
424
425#[derive(Debug, Clone, Default)]
427#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
428pub struct GetSparkStatusRequest {
429 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
433 pub proxy: Option<crate::ProxyConfig>,
434}
435
436#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
441pub async fn get_spark_status(
442 request: GetSparkStatusRequest,
443) -> Result<crate::SparkStatus, SdkError> {
444 use chrono::DateTime;
445
446 #[derive(serde::Deserialize)]
447 struct StatusApiResponse {
448 services: Vec<StatusApiService>,
449 #[serde(rename = "lastUpdated")]
450 last_updated: String,
451 }
452
453 #[derive(serde::Deserialize)]
454 struct StatusApiService {
455 name: String,
456 status: String,
457 }
458
459 fn parse_service_status(s: &str) -> crate::ServiceStatus {
460 match s {
461 "operational" => crate::ServiceStatus::Operational,
462 "degraded" => crate::ServiceStatus::Degraded,
463 "partial" => crate::ServiceStatus::Partial,
464 "major" => crate::ServiceStatus::Major,
465 _ => {
466 tracing::warn!("Unknown service status: {s}");
467 crate::ServiceStatus::Unknown
468 }
469 }
470 }
471
472 let http_client = crate::ProxyConfig::http_client(request.proxy.as_ref(), None)?;
473
474 let response = http_client
475 .get("https://spark.money/api/v1/status".to_string(), None)
476 .await
477 .map_err(|e| SdkError::NetworkError(e.to_string()))?;
478
479 let api_response: StatusApiResponse = response
480 .json()
481 .map_err(|e| SdkError::Generic(format!("Failed to parse status response: {e}")))?;
482
483 let status = api_response
484 .services
485 .iter()
486 .filter(|s| s.name == "Spark Operators" || s.name == "SSP")
487 .map(|s| parse_service_status(&s.status))
488 .max()
489 .unwrap_or(crate::ServiceStatus::Unknown);
490
491 let last_updated = DateTime::parse_from_rfc3339(&api_response.last_updated)
492 .map(|dt| dt.timestamp().cast_unsigned())
493 .map_err(|e| SdkError::Generic(format!("Failed to parse lastUpdated timestamp: {e}")))?;
494
495 Ok(crate::SparkStatus {
496 status,
497 last_updated,
498 })
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn default_server_config_disables_background_tasks() {
507 for network in [Network::Mainnet, Network::Regtest, Network::Signet] {
508 let cfg = default_server_config(network);
509 assert!(!cfg.background_tasks_enabled);
510 assert!(cfg.real_time_sync_server_url.is_none());
511 assert!(!cfg.leaf_optimization_config.auto_enabled);
512 assert!(!cfg.token_optimization_config.auto_enabled);
513 }
514 }
515
516 #[test]
517 fn default_config_enables_background_tasks() {
518 assert!(default_config(Network::Mainnet).background_tasks_enabled);
519 assert!(default_config(Network::Regtest).background_tasks_enabled);
520 assert!(default_config(Network::Signet).background_tasks_enabled);
521 }
522}