1mod api;
2mod contacts;
3mod deposits;
4mod helpers;
5mod init;
6mod lightning_address;
7mod lnurl;
8mod payments;
9mod sync;
10mod sync_coordinator;
11
12pub(crate) use sync_coordinator::SyncCoordinator;
13
14use bitflags::bitflags;
15use breez_sdk_common::{buy::BuyBitcoinProviderApi, fiat::FiatService, sync::SigningClient};
16use platform_utils::HttpClient;
17use platform_utils::tokio;
18use spark_wallet::SparkWallet;
19use std::sync::Arc;
20use tokio::sync::{Mutex, OnceCell, oneshot, watch};
21
22use crate::{
23 BitcoinChainService, ExternalInputParser, InputType, Logger, Network, OptimizationConfig,
24 error::SdkError, events::EventEmitter, lnurl::LnurlServerClient, logger, models::Config,
25 persist::Storage, signer::lnurl_auth::LnurlAuthSignerAdapter, stable_balance::StableBalance,
26 token_conversion::TokenConverter,
27};
28
29#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
30const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
31
32#[cfg(all(target_family = "wasm", target_os = "unknown"))]
33const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
34
35pub(crate) const CLAIM_TX_SIZE_VBYTES: u64 = 99;
36pub(crate) const SYNC_PAGING_LIMIT: u32 = 100;
37
38bitflags! {
39 #[derive(Clone, Debug, PartialEq, Eq)]
40 pub(crate) struct SyncType: u32 {
41 const Wallet = 1 << 0;
42 const WalletState = 1 << 1;
43 const Deposits = 1 << 2;
44 const LnurlMetadata = 1 << 3;
45 const Full = Self::Wallet.0.0
46 | Self::WalletState.0.0
47 | Self::Deposits.0.0
48 | Self::LnurlMetadata.0.0;
49 }
50}
51
52#[derive(Clone, Debug)]
53pub(crate) struct SyncRequest {
54 pub(crate) sync_type: SyncType,
55 #[allow(clippy::type_complexity)]
56 pub(crate) reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
57 pub(crate) force: bool,
60}
61
62impl SyncRequest {
63 pub(crate) async fn reply(&self, error: Option<SdkError>) {
64 if let Some(reply) = self.reply.lock().await.take() {
65 let _ = match error {
66 Some(e) => reply.send(Err(e)),
67 None => reply.send(Ok(())),
68 };
69 }
70 }
71}
72
73#[derive(Clone)]
76#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
77pub struct BreezSdk {
78 pub(crate) config: Config,
79 pub(crate) spark_wallet: Arc<SparkWallet>,
80 pub(crate) storage: Arc<dyn Storage>,
81 pub(crate) chain_service: Arc<dyn BitcoinChainService>,
82 pub(crate) fiat_service: Arc<dyn FiatService>,
83 pub(crate) lnurl_client: Arc<dyn HttpClient>,
84 pub(crate) lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
85 pub(crate) lnurl_auth_signer: Arc<LnurlAuthSignerAdapter>,
86 pub(crate) event_emitter: Arc<EventEmitter>,
87 pub(crate) shutdown_sender: watch::Sender<()>,
88 pub(crate) sync_coordinator: SyncCoordinator,
90 pub(crate) lnurl_preimage_trigger: tokio::sync::broadcast::Sender<()>,
91 pub(crate) initial_synced_watcher: watch::Receiver<bool>,
92 pub(crate) external_input_parsers: Vec<ExternalInputParser>,
93 pub(crate) spark_private_mode_initialized: Arc<OnceCell<()>>,
94 pub(crate) token_converter: Arc<dyn TokenConverter>,
95 pub(crate) stable_balance: Option<Arc<StableBalance>>,
96 pub(crate) buy_bitcoin_provider: Arc<dyn BuyBitcoinProviderApi>,
97}
98
99pub(crate) struct BreezSdkParams {
100 pub config: Config,
101 pub storage: Arc<dyn Storage>,
102 pub chain_service: Arc<dyn BitcoinChainService>,
103 pub fiat_service: Arc<dyn FiatService>,
104 pub lnurl_client: Arc<dyn HttpClient>,
105 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
106 pub lnurl_auth_signer: Arc<LnurlAuthSignerAdapter>,
107 pub shutdown_sender: watch::Sender<()>,
108 pub spark_wallet: Arc<SparkWallet>,
109 pub event_emitter: Arc<EventEmitter>,
110 pub sync_signing_client: Option<SigningClient>,
111 pub buy_bitcoin_provider: Arc<dyn BuyBitcoinProviderApi>,
112}
113
114pub async fn parse_input(
115 input: &str,
116 external_input_parsers: Option<Vec<ExternalInputParser>>,
117) -> Result<InputType, SdkError> {
118 Ok(breez_sdk_common::input::parse(
119 input,
120 external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
121 )
122 .await?
123 .into())
124}
125
126#[cfg_attr(feature = "uniffi", uniffi::export)]
127pub fn init_logging(
128 log_dir: Option<String>,
129 app_logger: Option<Box<dyn Logger>>,
130 log_filter: Option<String>,
131) -> Result<(), SdkError> {
132 logger::init_logging(log_dir, app_logger, log_filter)
133}
134
135#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
145#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
146pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
147 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
148 .with_default_storage(request.storage_dir);
149 let sdk = builder.build().await?;
150 Ok(sdk)
151}
152
153#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
166#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
167pub async fn connect_with_signer(
168 request: crate::ConnectWithSignerRequest,
169) -> Result<BreezSdk, SdkError> {
170 let builder = super::sdk_builder::SdkBuilder::new_with_signer(request.config, request.signer)
171 .with_default_storage(request.storage_dir);
172 let sdk = builder.build().await?;
173 Ok(sdk)
174}
175
176#[cfg_attr(feature = "uniffi", uniffi::export)]
177pub fn default_config(network: Network) -> Config {
178 let lnurl_domain = match network {
179 Network::Mainnet => Some("breez.tips".to_string()),
180 Network::Regtest => None,
181 };
182 Config {
183 api_key: None,
184 network,
185 sync_interval_secs: 60, max_deposit_claim_fee: Some(crate::MaxFee::Rate { sat_per_vbyte: 1 }),
187 lnurl_domain,
188 prefer_spark_over_lightning: false,
189 external_input_parsers: None,
190 use_default_external_input_parsers: true,
191 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
192 private_enabled_default: true,
193 optimization_config: OptimizationConfig {
194 auto_enabled: true,
195 multiplicity: 1,
196 },
197 stable_balance_config: None,
198 max_concurrent_claims: 4,
199 support_lnurl_verify: false,
200 }
201}
202
203#[cfg_attr(feature = "uniffi", uniffi::export)]
219pub fn default_external_signer(
220 mnemonic: String,
221 passphrase: Option<String>,
222 network: Network,
223 key_set_config: Option<crate::models::KeySetConfig>,
224) -> Result<Arc<dyn crate::signer::ExternalSigner>, SdkError> {
225 use crate::signer::DefaultExternalSigner;
226
227 let config = key_set_config.unwrap_or_default();
228 let signer = DefaultExternalSigner::new(
229 mnemonic,
230 passphrase,
231 network,
232 config.key_set_type,
233 config.use_address_index,
234 config.account_number,
235 )?;
236
237 Ok(Arc::new(signer))
238}
239
240#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
245pub async fn get_spark_status() -> Result<crate::SparkStatus, SdkError> {
246 use chrono::DateTime;
247 use platform_utils::DefaultHttpClient;
248
249 #[derive(serde::Deserialize)]
250 struct StatusApiResponse {
251 services: Vec<StatusApiService>,
252 #[serde(rename = "lastUpdated")]
253 last_updated: String,
254 }
255
256 #[derive(serde::Deserialize)]
257 struct StatusApiService {
258 name: String,
259 status: String,
260 }
261
262 fn parse_service_status(s: &str) -> crate::ServiceStatus {
263 match s {
264 "operational" => crate::ServiceStatus::Operational,
265 "degraded" => crate::ServiceStatus::Degraded,
266 "partial" => crate::ServiceStatus::Partial,
267 "major" => crate::ServiceStatus::Major,
268 _ => {
269 tracing::warn!("Unknown service status: {s}");
270 crate::ServiceStatus::Unknown
271 }
272 }
273 }
274
275 let http_client = DefaultHttpClient::default();
276
277 let response = http_client
278 .get("https://spark.money/api/v1/status".to_string(), None)
279 .await
280 .map_err(|e| SdkError::NetworkError(e.to_string()))?;
281
282 let api_response: StatusApiResponse = response
283 .json()
284 .map_err(|e| SdkError::Generic(format!("Failed to parse status response: {e}")))?;
285
286 let status = api_response
287 .services
288 .iter()
289 .filter(|s| s.name == "Spark Operators" || s.name == "SSP")
290 .map(|s| parse_service_status(&s.status))
291 .max()
292 .unwrap_or(crate::ServiceStatus::Unknown);
293
294 let last_updated = DateTime::parse_from_rfc3339(&api_response.last_updated)
295 .map(|dt| dt.timestamp().cast_unsigned())
296 .map_err(|e| SdkError::Generic(format!("Failed to parse lastUpdated timestamp: {e}")))?;
297
298 Ok(crate::SparkStatus {
299 status,
300 last_updated,
301 })
302}