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