breez_sdk_spark/sdk/
mod.rs

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    /// If true, bypass the "recently synced" check and sync immediately.
57    /// Use for event-driven syncs (after payments, transfers, etc.) that should happen immediately.
58    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/// `BreezSDK` is a wrapper around `SparkSDK` that provides a more structured API
73/// with request/response objects and comprehensive error handling.
74#[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    /// Coordinator for coalescing duplicate sync requests
88    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/// Connects to the Spark network using the provided configuration and mnemonic.
135///
136/// # Arguments
137///
138/// * `request` - The connection request object
139///
140/// # Returns
141///
142/// Result containing either the initialized `BreezSdk` or an `SdkError`
143#[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/// Connects to the Spark network using an external signer.
153///
154/// This method allows using a custom signer implementation instead of providing
155/// a seed directly.
156///
157/// # Arguments
158///
159/// * `request` - The connection request object with external signer
160///
161/// # Returns
162///
163/// Result containing either the initialized `BreezSdk` or an `SdkError`
164#[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, // every 1 minute
185        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/// Creates a default external signer from a mnemonic.
203///
204/// This is a convenience factory method for creating a signer that can be used
205/// with `connect_with_signer` or `SdkBuilder::new_with_signer`.
206///
207/// # Arguments
208///
209/// * `mnemonic` - BIP39 mnemonic phrase (12 or 24 words)
210/// * `passphrase` - Optional passphrase for the mnemonic
211/// * `network` - Network to use (Mainnet or Regtest)
212/// * `key_set_config` - Optional key set configuration. If None, uses default configuration.
213///
214/// # Returns
215///
216/// Result containing the signer as `Arc<dyn ExternalSigner>`
217#[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/// Fetches the current status of Spark network services relevant to the SDK.
240///
241/// This function queries the Spark status API and returns the worst status
242/// across the Spark Operators and SSP services.
243#[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}