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::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    /// 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) 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/// Connects to the Spark network using the provided configuration and mnemonic.
136///
137/// # Arguments
138///
139/// * `request` - The connection request object
140///
141/// # Returns
142///
143/// Result containing either the initialized `BreezSdk` or an `SdkError`
144#[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/// Connects to the Spark network using an external signer.
154///
155/// This method allows using a custom signer implementation instead of providing
156/// a seed directly.
157///
158/// # Arguments
159///
160/// * `request` - The connection request object with external signer
161///
162/// # Returns
163///
164/// Result containing either the initialized `BreezSdk` or an `SdkError`
165#[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, // every 1 minute
186        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/// Creates a default external signer from a mnemonic.
204///
205/// This is a convenience factory method for creating a signer that can be used
206/// with `connect_with_signer` or `SdkBuilder::new_with_signer`.
207///
208/// # Arguments
209///
210/// * `mnemonic` - BIP39 mnemonic phrase (12 or 24 words)
211/// * `passphrase` - Optional passphrase for the mnemonic
212/// * `network` - Network to use (Mainnet or Regtest)
213/// * `key_set_config` - Optional key set configuration. If None, uses default configuration.
214///
215/// # Returns
216///
217/// Result containing the signer as `Arc<dyn ExternalSigner>`
218#[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/// Fetches the current status of Spark network services relevant to the SDK.
241///
242/// This function queries the Spark status API and returns the worst status
243/// across the Spark Operators and SSP services.
244#[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}