breez_sdk_spark/sdk/
mod.rs

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::moonpay::MoonpayProvider, fiat::FiatService};
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    /// If true, bypass the "recently synced" check and sync immediately.
58    /// Use for event-driven syncs (after payments, transfers, etc.) that should happen immediately.
59    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/// `BreezSDK` is a wrapper around `SparkSDK` that provides a more structured API
74/// with request/response objects and comprehensive error handling.
75#[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    /// Coordinator for coalescing duplicate sync requests
89    pub(crate) sync_coordinator: SyncCoordinator,
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<MoonpayProvider>,
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 buy_bitcoin_provider: Arc<MoonpayProvider>,
110    pub token_converter: Arc<dyn TokenConverter>,
111    pub stable_balance: Option<Arc<StableBalance>>,
112    pub sync_coordinator: SyncCoordinator,
113}
114
115pub async fn parse_input(
116    input: &str,
117    external_input_parsers: Option<Vec<ExternalInputParser>>,
118) -> Result<InputType, SdkError> {
119    Ok(breez_sdk_common::input::parse(
120        input,
121        external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
122    )
123    .await?
124    .into())
125}
126
127#[cfg_attr(feature = "uniffi", uniffi::export)]
128pub fn init_logging(
129    log_dir: Option<String>,
130    app_logger: Option<Box<dyn Logger>>,
131    log_filter: Option<String>,
132) -> Result<(), SdkError> {
133    logger::init_logging(log_dir, app_logger, log_filter)
134}
135
136/// Connects to the Spark network using the provided configuration and mnemonic.
137///
138/// # Arguments
139///
140/// * `request` - The connection request object
141///
142/// # Returns
143///
144/// Result containing either the initialized `BreezSdk` or an `SdkError`
145#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
146#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
147pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
148    let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
149        .with_default_storage(request.storage_dir);
150    let sdk = builder.build().await?;
151    Ok(sdk)
152}
153
154/// Connects to the Spark network using an external signer.
155///
156/// This method allows using a custom signer implementation instead of providing
157/// a seed directly.
158///
159/// # Arguments
160///
161/// * `request` - The connection request object with external signer
162///
163/// # Returns
164///
165/// Result containing either the initialized `BreezSdk` or an `SdkError`
166#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
167#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
168pub async fn connect_with_signer(
169    request: crate::ConnectWithSignerRequest,
170) -> Result<BreezSdk, SdkError> {
171    let builder = super::sdk_builder::SdkBuilder::new_with_signer(request.config, request.signer)
172        .with_default_storage(request.storage_dir);
173    let sdk = builder.build().await?;
174    Ok(sdk)
175}
176
177#[cfg_attr(feature = "uniffi", uniffi::export)]
178pub fn default_config(network: Network) -> Config {
179    let lnurl_domain = match network {
180        Network::Mainnet => Some("breez.tips".to_string()),
181        Network::Regtest => None,
182    };
183    Config {
184        api_key: None,
185        network,
186        sync_interval_secs: 60, // every 1 minute
187        max_deposit_claim_fee: Some(crate::MaxFee::Rate { sat_per_vbyte: 1 }),
188        lnurl_domain,
189        prefer_spark_over_lightning: false,
190        external_input_parsers: None,
191        use_default_external_input_parsers: true,
192        real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
193        private_enabled_default: true,
194        optimization_config: OptimizationConfig {
195            auto_enabled: true,
196            multiplicity: 1,
197        },
198        stable_balance_config: None,
199        max_concurrent_claims: 4,
200        spark_config: None,
201    }
202}
203
204/// Creates a default external signer from a mnemonic.
205///
206/// This is a convenience factory method for creating a signer that can be used
207/// with `connect_with_signer` or `SdkBuilder::new_with_signer`.
208///
209/// # Arguments
210///
211/// * `mnemonic` - BIP39 mnemonic phrase (12 or 24 words)
212/// * `passphrase` - Optional passphrase for the mnemonic
213/// * `network` - Network to use (Mainnet or Regtest)
214/// * `key_set_config` - Optional key set configuration. If None, uses default configuration.
215///
216/// # Returns
217///
218/// Result containing the signer as `Arc<dyn ExternalSigner>`
219#[cfg_attr(feature = "uniffi", uniffi::export)]
220pub fn default_external_signer(
221    mnemonic: String,
222    passphrase: Option<String>,
223    network: Network,
224    key_set_config: Option<crate::models::KeySetConfig>,
225) -> Result<Arc<dyn crate::signer::ExternalSigner>, SdkError> {
226    use crate::signer::DefaultExternalSigner;
227
228    let config = key_set_config.unwrap_or_default();
229    let signer = DefaultExternalSigner::new(
230        mnemonic,
231        passphrase,
232        network,
233        config.key_set_type,
234        config.use_address_index,
235        config.account_number,
236    )?;
237
238    Ok(Arc::new(signer))
239}
240
241/// Fetches the current status of Spark network services relevant to the SDK.
242///
243/// This function queries the Spark status API and returns the worst status
244/// across the Spark Operators and SSP services.
245#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
246pub async fn get_spark_status() -> Result<crate::SparkStatus, SdkError> {
247    use chrono::DateTime;
248    use platform_utils::DefaultHttpClient;
249
250    #[derive(serde::Deserialize)]
251    struct StatusApiResponse {
252        services: Vec<StatusApiService>,
253        #[serde(rename = "lastUpdated")]
254        last_updated: String,
255    }
256
257    #[derive(serde::Deserialize)]
258    struct StatusApiService {
259        name: String,
260        status: String,
261    }
262
263    fn parse_service_status(s: &str) -> crate::ServiceStatus {
264        match s {
265            "operational" => crate::ServiceStatus::Operational,
266            "degraded" => crate::ServiceStatus::Degraded,
267            "partial" => crate::ServiceStatus::Partial,
268            "major" => crate::ServiceStatus::Major,
269            _ => {
270                tracing::warn!("Unknown service status: {s}");
271                crate::ServiceStatus::Unknown
272            }
273        }
274    }
275
276    let http_client = DefaultHttpClient::default();
277
278    let response = http_client
279        .get("https://spark.money/api/v1/status".to_string(), None)
280        .await
281        .map_err(|e| SdkError::NetworkError(e.to_string()))?;
282
283    let api_response: StatusApiResponse = response
284        .json()
285        .map_err(|e| SdkError::Generic(format!("Failed to parse status response: {e}")))?;
286
287    let status = api_response
288        .services
289        .iter()
290        .filter(|s| s.name == "Spark Operators" || s.name == "SSP")
291        .map(|s| parse_service_status(&s.status))
292        .max()
293        .unwrap_or(crate::ServiceStatus::Unknown);
294
295    let last_updated = DateTime::parse_from_rfc3339(&api_response.last_updated)
296        .map(|dt| dt.timestamp().cast_unsigned())
297        .map_err(|e| SdkError::Generic(format!("Failed to parse lastUpdated timestamp: {e}")))?;
298
299    Ok(crate::SparkStatus {
300        status,
301        last_updated,
302    })
303}