1mod api;
2mod bridging;
3mod buy;
4mod chain_queries;
5mod contacts;
6mod deposits;
7pub(crate) mod exit_chain_downloader;
8mod helpers;
9mod init;
10mod lightning_address;
11mod lightning_sender;
12mod lnurl;
13mod payments;
14mod recover_funds;
15mod runtime;
16mod sync;
17mod sync_coordinator;
18mod token_allowances;
19mod unilateral_exit;
20mod unilateral_exit_backup;
21mod watchtower_exit;
22
23pub use bridging::{
24 BridgeRoutePair, BridgeRoutePairSide, BridgeStatus, BridgingClient, BridgingConfig,
25 CreateBridgeRequest, CreateBridgeResponse, GetBridgeRoutesRequest, GetBridgeRoutesResponse,
26 GetBridgeStatusRequest, GetBridgeStatusResponse, default_bridging_config, new_bridging_client,
27};
28pub(crate) use helpers::partner_id;
29pub(crate) use lightning_sender::{LightningSender, PendingLightningSend};
30pub(crate) use runtime::{RuntimeEvent, SdkRuntime, runtime_from_config};
31pub(crate) use sync_coordinator::SyncCoordinator;
32
33use bitflags::bitflags;
34use breez_sdk_common::{buy::moonpay::MoonpayProvider, fiat::FiatService};
35use platform_utils::HttpClient;
36use platform_utils::tokio;
37use spark_wallet::SparkWallet;
38use std::sync::Arc;
39use tokio::sync::{Mutex, OnceCell, oneshot, watch};
40
41use crate::{
42 BitcoinChainService, LeafOptimizationConfig, Logger, Network, SyncError,
43 TokenOptimizationConfig, error::SdkError, events::EventEmitter, lnurl::LnurlServerClient,
44 logger, models::Config, models::SparkConfig, persist::Storage, rewards::RewardsDelegator,
45 signer::lnurl_auth::LnurlAuthSignerAdapter, stable_balance::StableBalance,
46 token_conversion::TokenConverter,
47};
48
49#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
50const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
51
52#[cfg(all(target_family = "wasm", target_os = "unknown"))]
53const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
54
55pub(crate) const CLAIM_TX_SIZE_VBYTES: u64 = 99;
56pub(crate) const SYNC_PAGING_LIMIT: u32 = 100;
57
58bitflags! {
59 #[derive(Clone, Debug, PartialEq, Eq)]
60 pub(crate) struct SyncType: u32 {
61 const Wallet = 1 << 0;
62 const WalletState = 1 << 1;
63 const Deposits = 1 << 2;
64 const LnurlMetadata = 1 << 3;
65 const ExitChainState = 1 << 4;
66 const Full = Self::Wallet.0.0
67 | Self::WalletState.0.0
68 | Self::Deposits.0.0
69 | Self::LnurlMetadata.0.0
70 | Self::ExitChainState.0.0;
71 }
72}
73
74#[derive(Clone, Debug)]
75pub(crate) struct SyncRequest {
76 pub(crate) sync_type: SyncType,
77 #[allow(clippy::type_complexity)]
78 pub(crate) reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
79 pub(crate) force: bool,
82}
83
84impl SyncRequest {
85 pub(crate) async fn reply(&self, error: Option<SdkError>) {
86 if let Some(reply) = self.reply.lock().await.take() {
87 let _ = match error {
88 Some(e) => reply.send(Err(e)),
89 None => reply.send(Ok(())),
90 };
91 }
92 }
93}
94
95#[derive(Clone)]
98#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
99pub struct BreezSdk {
100 pub(crate) instance_id: String,
102 pub(crate) config: Config,
103 pub(crate) spark_wallet: Arc<SparkWallet>,
104 pub(crate) storage: Arc<dyn Storage>,
105 pub(crate) chain_service: Arc<dyn BitcoinChainService>,
106 pub(crate) fiat_service: Arc<dyn FiatService>,
107 pub(crate) lnurl_client: Arc<dyn HttpClient>,
108 pub(crate) lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
109 pub(crate) lnurl_auth_signer: Option<Arc<LnurlAuthSignerAdapter>>,
110 pub(crate) event_emitter: Arc<EventEmitter>,
111 pub(crate) shutdown_sender: watch::Sender<()>,
112 pub(crate) exit_chain_trigger: exit_chain_downloader::ExitChainTrigger,
115 pub(crate) runtime: SdkRuntime,
116 pub(crate) sync_coordinator: SyncCoordinator,
118 pub(crate) claim_guards: deposits::ClaimGuards,
121 pub(crate) initial_synced_watcher: watch::Receiver<bool>,
122 pub(crate) last_sync_error: Arc<Mutex<Option<SyncError>>>,
124 pub(crate) input_parser: Arc<SdkInputParser>,
128 pub(crate) spark_private_mode_initialized: Arc<OnceCell<()>>,
129 pub(crate) token_converter: Arc<dyn TokenConverter>,
130 pub(crate) rewards_delegator: Option<Arc<RewardsDelegator>>,
132 pub(crate) stable_balance: Option<Arc<StableBalance>>,
133 pub(crate) buy_bitcoin_provider: Arc<MoonpayProvider>,
134 pub(crate) cross_chain_context: crate::cross_chain::CrossChainContext,
135 pub(crate) orchestra: Option<Arc<crate::cross_chain::OrchestraService>>,
138 #[allow(dead_code)]
142 pub(crate) lightning_sender: Arc<LightningSender>,
143}
144
145pub(crate) type SdkInputParser =
147 breez_sdk_common::input::InputParser<Arc<dyn HttpClient>, breez_sdk_common::dns::Resolver>;
148
149pub(crate) struct BreezSdkParams {
150 pub instance_id: String,
151 pub config: Config,
152 pub input_parser: Arc<SdkInputParser>,
153 pub storage: Arc<dyn Storage>,
154 pub chain_service: Arc<dyn BitcoinChainService>,
155 pub fiat_service: Arc<dyn FiatService>,
156 pub lnurl_client: Arc<dyn HttpClient>,
157 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
158 pub lnurl_auth_signer: Option<Arc<LnurlAuthSignerAdapter>>,
159 pub shutdown_sender: watch::Sender<()>,
160 pub runtime: SdkRuntime,
161 pub spark_wallet: Arc<SparkWallet>,
162 pub event_emitter: Arc<EventEmitter>,
163 pub buy_bitcoin_provider: Arc<MoonpayProvider>,
164 pub token_converter: Arc<dyn TokenConverter>,
165 pub rewards_delegator: Option<Arc<RewardsDelegator>>,
166 pub stable_balance: Option<Arc<StableBalance>>,
167 pub sync_coordinator: SyncCoordinator,
168 pub cross_chain_context: crate::cross_chain::CrossChainContext,
169 pub orchestra: Option<Arc<crate::cross_chain::OrchestraService>>,
170 pub lightning_sender: Arc<LightningSender>,
171}
172
173#[allow(clippy::needless_pass_by_value)]
174#[cfg_attr(feature = "uniffi", uniffi::export)]
175pub fn init_logging(
176 log_dir: Option<String>,
177 app_logger: Option<Box<dyn Logger>>,
178 log_filter: Option<String>,
179) -> Result<(), SdkError> {
180 logger::init_logging(log_dir.as_deref(), app_logger, log_filter.as_deref())
181}
182
183#[cfg(feature = "sqlite")]
193#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
194pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
195 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
196 .with_default_storage(request.storage_dir);
197 let sdk = builder.build().await?;
198 Ok(sdk)
199}
200
201#[cfg(feature = "sqlite")]
214#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
215pub async fn connect_with_signer(
216 request: crate::ConnectWithSignerRequest,
217) -> Result<BreezSdk, SdkError> {
218 let builder = super::sdk_builder::SdkBuilder::new_with_signer(
219 request.config,
220 request.breez_signer,
221 request.spark_signer,
222 )
223 .with_default_storage(request.storage_dir);
224 let sdk = builder.build().await?;
225 Ok(sdk)
226}
227
228#[cfg(feature = "sqlite")]
243#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
244pub async fn connect_with_signing_only_signer(
245 request: crate::ConnectWithSigningOnlySignerRequest,
246) -> Result<BreezSdk, SdkError> {
247 let builder = super::sdk_builder::SdkBuilder::new_with_signing_only_signer(
248 request.config,
249 request.breez_signer,
250 request.spark_signer,
251 )
252 .with_default_storage(request.storage_dir);
253 let sdk = builder.build().await?;
254 Ok(sdk)
255}
256
257#[cfg_attr(feature = "uniffi", uniffi::export)]
261#[allow(clippy::needless_pass_by_value)]
263pub fn parse_spark_config(json: String) -> Result<SparkConfig, SdkError> {
264 serde_json::from_str(&json).map_err(|e| SdkError::InvalidInput(e.to_string()))
265}
266
267#[cfg_attr(feature = "uniffi", uniffi::export)]
268pub fn default_config(network: Network) -> Config {
269 let lnurl_domain = match network {
270 Network::Mainnet => Some("breez.tips".to_string()),
271 Network::Signet | Network::Regtest => None,
272 };
273 Config {
274 api_key: None,
275 network,
276 sync_interval_secs: 60, max_deposit_claim_fee: Some(crate::MaxFee {
278 onchain: crate::MaxOnchainFee::Rate { sat_per_vbyte: 1 },
279 proportional_ppm: Some(1_000),
280 }),
281 lnurl_domain,
282 prefer_spark_over_lightning: false,
283 exit_chain_auto_fetch_enabled: true,
284 watchtower_fee_ladder_enabled: None,
285 external_input_parsers: None,
286 use_default_external_input_parsers: true,
287 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
288 private_enabled_default: true,
289 leaf_optimization_config: LeafOptimizationConfig {
290 auto_enabled: true,
291 multiplicity: 1,
292 },
293 token_optimization_config: TokenOptimizationConfig {
294 auto_enabled: true,
295 target_output_count: 5,
296 min_outputs_threshold: 50,
297 },
298 stable_balance_config: None,
299 max_concurrent_claims: 4,
300 spark_config: match network {
301 Network::Mainnet | Network::Regtest => Some(default_spark_config(network)),
302 Network::Signet => None,
303 },
304 background_tasks_enabled: true,
305 proxy: None,
306 cross_chain_config: None,
307 }
308}
309
310#[cfg_attr(feature = "uniffi", uniffi::export)]
344pub fn default_server_config(network: Network) -> Config {
345 let mut config = default_config(network);
346 config.background_tasks_enabled = false;
347 config.real_time_sync_server_url = None;
348 config.leaf_optimization_config.auto_enabled = false;
349 config.token_optimization_config.auto_enabled = false;
350 config.cross_chain_config = None;
351 config
352}
353
354fn default_spark_config(network: Network) -> crate::models::SparkConfig {
360 use crate::models::{SparkSigningOperator, SparkSspConfig};
361
362 let wallet_config = spark_wallet::SparkWalletConfig::default_config(network.into());
363
364 let coordinator_identifier = hex::encode(
365 wallet_config
366 .operator_pool
367 .get_coordinator()
368 .identifier
369 .serialize(),
370 );
371
372 let signing_operators = wallet_config
373 .operator_pool
374 .get_all_operators()
375 .map(|op| SparkSigningOperator {
376 id: u32::try_from(op.id).expect("operator id fits in u32"),
377 identifier: hex::encode(op.identifier.serialize()),
378 address: op.address.clone(),
379 identity_public_key: hex::encode(op.identity_public_key.serialize()),
380 ca_cert_pem: op
381 .ca_cert
382 .as_ref()
383 .and_then(|b| String::from_utf8(b.clone()).ok()),
384 })
385 .collect();
386
387 let ssp = &wallet_config.service_provider_config;
388
389 crate::models::SparkConfig {
390 coordinator_identifier,
391 threshold: wallet_config.split_secret_threshold,
392 signing_operators,
393 ssp_config: SparkSspConfig {
394 base_url: ssp.base_url.clone(),
395 identity_public_key: hex::encode(ssp.identity_public_key.serialize()),
396 schema_endpoint: ssp.schema_endpoint.clone(),
397 },
398 expected_withdraw_bond_sats: wallet_config.tokens_config.expected_withdraw_bond_sats,
399 expected_withdraw_relative_block_locktime: wallet_config
400 .tokens_config
401 .expected_withdraw_relative_block_locktime,
402 max_token_transaction_inputs: None,
403 }
404}
405
406#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
409pub struct ExternalSigners {
410 pub breez_signer: Arc<dyn crate::signer::ExternalBreezSigner>,
413 pub spark_signer: Arc<dyn crate::signer::ExternalSparkSigner>,
415}
416
417#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
421pub struct SigningOnlyExternalSigners {
422 pub breez_signer: Arc<dyn crate::signer::ExternalSigningSigner>,
424 pub spark_signer: Arc<dyn crate::signer::ExternalSparkSigner>,
426}
427
428#[cfg_attr(feature = "uniffi", uniffi::export)]
443pub fn default_external_signers(
444 mnemonic: String,
445 passphrase: Option<String>,
446 network: Network,
447 account_number: Option<u32>,
448) -> Result<ExternalSigners, SdkError> {
449 use crate::signer::{DefaultExternalSigner, DefaultExternalSparkSigner};
450
451 let breez_signer = DefaultExternalSigner::new(
452 mnemonic.clone(),
453 passphrase.clone(),
454 network,
455 account_number,
456 )?;
457 let spark_signer =
458 DefaultExternalSparkSigner::new(mnemonic, passphrase, network, account_number)?;
459
460 Ok(ExternalSigners {
461 breez_signer: Arc::new(breez_signer),
462 spark_signer: Arc::new(spark_signer),
463 })
464}
465
466#[derive(Debug, Clone, Default)]
468#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
469pub struct GetSparkStatusRequest {
470 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
474 pub proxy: Option<crate::ProxyConfig>,
475}
476
477#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
482pub async fn get_spark_status(
483 request: GetSparkStatusRequest,
484) -> Result<crate::SparkStatus, SdkError> {
485 use chrono::DateTime;
486
487 #[derive(serde::Deserialize)]
488 struct StatusApiResponse {
489 services: Vec<StatusApiService>,
490 #[serde(rename = "lastUpdated")]
491 last_updated: String,
492 }
493
494 #[derive(serde::Deserialize)]
495 struct StatusApiService {
496 name: String,
497 status: String,
498 }
499
500 fn parse_service_status(s: &str) -> crate::ServiceStatus {
501 match s {
502 "operational" => crate::ServiceStatus::Operational,
503 "degraded" => crate::ServiceStatus::Degraded,
504 "partial" => crate::ServiceStatus::Partial,
505 "major" => crate::ServiceStatus::Major,
506 _ => {
507 tracing::warn!("Unknown service status: {s}");
508 crate::ServiceStatus::Unknown
509 }
510 }
511 }
512
513 let http_client = crate::ProxyConfig::http_client(request.proxy.as_ref(), None)?;
514
515 let response = http_client
516 .get("https://spark.money/api/v1/status".to_string(), None)
517 .await
518 .map_err(|e| SdkError::NetworkError(e.to_string()))?;
519
520 let api_response: StatusApiResponse = response
521 .json()
522 .map_err(|e| SdkError::Generic(format!("Failed to parse status response: {e}")))?;
523
524 let status = api_response
525 .services
526 .iter()
527 .filter(|s| s.name == "Spark Operators" || s.name == "SSP")
528 .map(|s| parse_service_status(&s.status))
529 .max()
530 .unwrap_or(crate::ServiceStatus::Unknown);
531
532 let last_updated = DateTime::parse_from_rfc3339(&api_response.last_updated)
533 .map(|dt| dt.timestamp().cast_unsigned())
534 .map_err(|e| SdkError::Generic(format!("Failed to parse lastUpdated timestamp: {e}")))?;
535
536 Ok(crate::SparkStatus {
537 status,
538 last_updated,
539 })
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn default_server_config_disables_background_tasks() {
548 for network in [Network::Mainnet, Network::Regtest, Network::Signet] {
549 let cfg = default_server_config(network);
550 assert!(!cfg.background_tasks_enabled);
551 assert!(cfg.real_time_sync_server_url.is_none());
552 assert!(!cfg.leaf_optimization_config.auto_enabled);
553 assert!(!cfg.token_optimization_config.auto_enabled);
554 }
555 }
556
557 #[test]
558 fn default_config_enables_background_tasks() {
559 assert!(default_config(Network::Mainnet).background_tasks_enabled);
560 assert!(default_config(Network::Regtest).background_tasks_enabled);
561 assert!(default_config(Network::Signet).background_tasks_enabled);
562 }
563
564 #[test]
566 fn parse_spark_config_reads_an_environments_file() {
567 let json = r#"{
568 "coordinator_identifier": "01",
569 "threshold": 2,
570 "signing_operators": [
571 {
572 "id": 0,
573 "identifier": "01",
574 "address": "http://127.0.0.1:8535",
575 "identity_public_key": "02aa"
576 }
577 ],
578 "ssp_config": {
579 "base_url": "http://127.0.0.1:59049",
580 "identity_public_key": "03bb",
581 "schema_endpoint": "graphql/spark/rc"
582 },
583 "expected_withdraw_bond_sats": 10000,
584 "expected_withdraw_relative_block_locktime": 1000
585 }"#;
586
587 let config = parse_spark_config(json.to_string()).unwrap();
588
589 assert_eq!(config.threshold, 2);
590 assert_eq!(config.signing_operators.len(), 1);
591 assert_eq!(config.signing_operators[0].address, "http://127.0.0.1:8535");
592 assert_eq!(config.ssp_config.base_url, "http://127.0.0.1:59049");
593 assert!(config.signing_operators[0].ca_cert_pem.is_none());
595 assert!(config.max_token_transaction_inputs.is_none());
596 }
597
598 #[test]
599 fn parse_spark_config_rejects_what_is_not_one() {
600 assert!(parse_spark_config("{}".to_string()).is_err());
601 }
602
603 fn has_public_method(body: &[&str]) -> bool {
605 let next_lines = body.iter().skip(1).map(Some).chain(std::iter::once(None));
606 body.iter().zip(next_lines).any(|(line, next)| {
607 let Some(signature) = line.strip_prefix(" pub ") else {
608 return false;
609 };
610 if !signature.starts_with("fn ") && !signature.starts_with("async fn ") {
611 return false;
612 }
613 line.contains("self") || next.is_some_and(|next| next.contains("self"))
615 })
616 }
617
618 fn rust_files(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf>) {
619 for entry in std::fs::read_dir(dir).unwrap() {
620 let path = entry.unwrap().path();
621 if path.is_dir() {
622 rust_files(&path, files);
623 } else if path.extension().is_some_and(|ext| ext == "rs") {
624 files.push(path);
625 }
626 }
627 }
628
629 #[test]
633 fn impl_blocks_with_public_methods_have_an_instance_span() {
634 let mut files = Vec::new();
635 rust_files(
636 &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
637 &mut files,
638 );
639 let mut missing = Vec::new();
640 for path in files {
641 let source = std::fs::read_to_string(&path).unwrap();
642 let lines: Vec<&str> = source.lines().collect();
643 for (i, line) in lines.iter().enumerate() {
644 if !line.starts_with("impl BreezSdk") {
645 continue;
646 }
647 let body: Vec<&str> = lines[i..]
648 .iter()
649 .skip(1)
650 .copied()
651 .take_while(|l| *l != "}")
652 .collect();
653 if !has_public_method(&body) {
654 continue;
655 }
656 let has_attribute = lines[..i]
657 .iter()
658 .rev()
659 .take_while(|l| l.starts_with("#[") || l.starts_with("///"))
660 .any(|l| *l == "#[macros::instance_span]");
661 if !has_attribute {
662 missing.push(format!("{}:{}", path.display(), i.saturating_add(1)));
663 }
664 }
665 }
666 assert!(
667 missing.is_empty(),
668 "impl blocks missing #[macros::instance_span]: {missing:?}"
669 );
670 }
671}