breez_sdk_spark/
sdk_builder.rs

1#![cfg_attr(
2    all(target_family = "wasm", target_os = "unknown"),
3    allow(clippy::arc_with_non_send_sync)
4)]
5use std::sync::Arc;
6
7use breez_sdk_common::{
8    breez_server::{BreezServer, PRODUCTION_BREEZSERVER_URL},
9    rest::ReqwestRestClient as CommonRequestRestClient,
10};
11
12use flashnet::{CacheStore, FlashnetClient, FlashnetConfig};
13#[cfg(not(target_family = "wasm"))]
14use spark_wallet::Signer;
15use tokio::sync::watch;
16use tracing::{debug, info};
17
18use crate::{
19    Credentials, EventEmitter, FiatService, FiatServiceWrapper, KeySetType, Network, RestClient,
20    RestClientWrapper, Seed,
21    chain::{
22        BitcoinChainService,
23        rest_client::{BasicAuth, ChainApiType, RestClientChainService},
24    },
25    error::SdkError,
26    lnurl::{LnurlServerClient, ReqwestLnurlServerClient},
27    models::Config,
28    nostr::NostrClient,
29    payment_observer::{PaymentObserver, SparkTransferObserver},
30    persist::Storage,
31    realtime_sync::{RealTimeSyncParams, init_and_start_real_time_sync},
32    sdk::{BreezSdk, BreezSdkParams},
33    signer::{
34        breez::BreezSignerImpl, nostr::NostrSigner, rtsync::RTSyncSigner, spark::SparkSigner,
35    },
36    sync_storage::SyncStorage,
37};
38
39/// Source for the signer - either a seed or an external signer implementation
40#[derive(Clone)]
41enum SignerSource {
42    Seed {
43        seed: Seed,
44        key_set_type: KeySetType,
45        use_address_index: bool,
46        account_number: Option<u32>,
47    },
48    External(Arc<dyn crate::signer::ExternalSigner>),
49}
50
51/// Builder for creating `BreezSdk` instances with customizable components.
52#[derive(Clone)]
53pub struct SdkBuilder {
54    config: Config,
55    signer_source: SignerSource,
56
57    storage_dir: Option<String>,
58    storage: Option<Arc<dyn Storage>>,
59    chain_service: Option<Arc<dyn BitcoinChainService>>,
60    fiat_service: Option<Arc<dyn FiatService>>,
61    lnurl_client: Option<Arc<dyn RestClient>>,
62    lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
63    payment_observer: Option<Arc<dyn PaymentObserver>>,
64    sync_storage: Option<Arc<dyn SyncStorage>>,
65}
66
67impl SdkBuilder {
68    /// Creates a new `SdkBuilder` with the provided configuration and seed.
69    ///
70    /// For external signer support, use `new_with_signer` instead.
71    ///
72    /// # Arguments
73    /// - `config`: The configuration to be used.
74    /// - `seed`: The seed for wallet generation.
75    #[allow(clippy::needless_pass_by_value)]
76    pub fn new(config: Config, seed: Seed) -> Self {
77        SdkBuilder {
78            config,
79            signer_source: SignerSource::Seed {
80                seed,
81                key_set_type: KeySetType::Default,
82                use_address_index: false,
83                account_number: None,
84            },
85            storage_dir: None,
86            storage: None,
87            chain_service: None,
88            fiat_service: None,
89            lnurl_client: None,
90            lnurl_server_client: None,
91            payment_observer: None,
92            sync_storage: None,
93        }
94    }
95
96    /// Creates a new `SdkBuilder` with the provided configuration and external signer.
97    ///
98    /// # Arguments
99    /// - `config`: The configuration to be used.
100    /// - `signer`: An external signer implementation.
101    #[allow(clippy::needless_pass_by_value)]
102    pub fn new_with_signer(config: Config, signer: Arc<dyn crate::signer::ExternalSigner>) -> Self {
103        SdkBuilder {
104            config,
105            signer_source: SignerSource::External(signer),
106            storage_dir: None,
107            storage: None,
108            chain_service: None,
109            fiat_service: None,
110            lnurl_client: None,
111            lnurl_server_client: None,
112            payment_observer: None,
113            sync_storage: None,
114        }
115    }
116
117    /// Sets the key set type to be used by the SDK.
118    ///
119    /// Note: This only applies when using a seed-based signer. It has no effect
120    /// when using an external signer (created with `new_with_signer`).
121    ///
122    /// # Arguments
123    /// - `config`: Key set configuration containing the key set type, address index flag, and optional account number.
124    #[must_use]
125    pub fn with_key_set(mut self, config: crate::models::KeySetConfig) -> Self {
126        if let SignerSource::Seed {
127            key_set_type: ref mut kst,
128            use_address_index: ref mut uai,
129            account_number: ref mut an,
130            ..
131        } = self.signer_source
132        {
133            *kst = config.key_set_type;
134            *uai = config.use_address_index;
135            *an = config.account_number;
136        }
137        self
138    }
139
140    #[must_use]
141    /// Sets the root storage directory to initialize the default storage with.
142    /// This initializes both storage and real-time sync storage with the
143    /// default implementations.
144    /// Arguments:
145    /// - `storage_dir`: The data directory for storage.
146    pub fn with_default_storage(mut self, storage_dir: String) -> Self {
147        self.storage_dir = Some(storage_dir);
148        self
149    }
150
151    #[must_use]
152    /// Sets the storage implementation to be used by the SDK.
153    /// Arguments:
154    /// - `storage`: The storage implementation to be used.
155    pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
156        self.storage = Some(storage);
157        self
158    }
159
160    #[must_use]
161    /// Sets the real-time sync storage implementation to be used by the SDK.
162    /// Arguments:
163    /// - `storage`: The sync storage implementation to be used.
164    pub fn with_real_time_sync_storage(mut self, storage: Arc<dyn SyncStorage>) -> Self {
165        self.sync_storage = Some(storage);
166        self
167    }
168
169    /// Sets the chain service to be used by the SDK.
170    /// Arguments:
171    /// - `chain_service`: The chain service to be used.
172    #[must_use]
173    pub fn with_chain_service(mut self, chain_service: Arc<dyn BitcoinChainService>) -> Self {
174        self.chain_service = Some(chain_service);
175        self
176    }
177
178    /// Sets the REST chain service to be used by the SDK.
179    /// Arguments:
180    /// - `url`: The base URL of the REST API.
181    /// - `api_type`: The API type to be used.
182    /// - `credentials`: Optional credentials for basic authentication.
183    #[must_use]
184    pub fn with_rest_chain_service(
185        mut self,
186        url: String,
187        api_type: ChainApiType,
188        credentials: Option<Credentials>,
189    ) -> Self {
190        self.chain_service = Some(Arc::new(RestClientChainService::new(
191            url,
192            self.config.network,
193            5,
194            Box::new(CommonRequestRestClient::new().unwrap()),
195            credentials.map(|c| BasicAuth::new(c.username, c.password)),
196            api_type,
197        )));
198        self
199    }
200
201    /// Sets the fiat service to be used by the SDK.
202    /// Arguments:
203    /// - `fiat_service`: The fiat service to be used.
204    #[must_use]
205    pub fn with_fiat_service(mut self, fiat_service: Arc<dyn FiatService>) -> Self {
206        self.fiat_service = Some(fiat_service);
207        self
208    }
209
210    #[must_use]
211    pub fn with_lnurl_client(mut self, lnurl_client: Arc<dyn RestClient>) -> Self {
212        self.lnurl_client = Some(lnurl_client);
213        self
214    }
215
216    #[must_use]
217    #[allow(unused)]
218    pub fn with_lnurl_server_client(
219        mut self,
220        lnurl_serverclient: Arc<dyn LnurlServerClient>,
221    ) -> Self {
222        self.lnurl_server_client = Some(lnurl_serverclient);
223        self
224    }
225
226    /// Sets the payment observer to be used by the SDK.
227    /// This observer will receive callbacks before outgoing payments for Lightning, Spark and onchain Bitcoin.
228    /// Arguments:
229    /// - `payment_observer`: The payment observer to be used.
230    #[must_use]
231    #[allow(unused)]
232    pub fn with_payment_observer(mut self, payment_observer: Arc<dyn PaymentObserver>) -> Self {
233        self.payment_observer = Some(payment_observer);
234        self
235    }
236
237    /// Builds the `BreezSdk` instance with the configured components.
238    #[allow(clippy::too_many_lines)]
239    pub async fn build(self) -> Result<BreezSdk, SdkError> {
240        // Create the base signer based on the signer source
241        let (signer, account_number) = match self.signer_source {
242            SignerSource::Seed {
243                seed,
244                key_set_type,
245                use_address_index,
246                account_number,
247            } => {
248                let breez_signer = Arc::new(
249                    BreezSignerImpl::new(
250                        &self.config,
251                        &seed,
252                        key_set_type.into(),
253                        use_address_index,
254                        account_number,
255                    )
256                    .map_err(|e| SdkError::Generic(e.to_string()))?,
257                );
258                (
259                    breez_signer as Arc<dyn crate::signer::BreezSigner>,
260                    account_number,
261                )
262            }
263            SignerSource::External(external_signer) => {
264                use crate::signer::ExternalSignerAdapter;
265                let adapter = Arc::new(ExternalSignerAdapter::new(external_signer));
266                (adapter as Arc<dyn crate::signer::BreezSigner>, None)
267            }
268        };
269
270        // Create the specialized signers
271        let spark_signer = Arc::new(SparkSigner::new(signer.clone()));
272        let rtsync_signer = Arc::new(
273            RTSyncSigner::new(signer.clone(), self.config.network)
274                .map_err(|e| SdkError::Generic(e.to_string()))?,
275        );
276        let nostr_signer = Arc::new(
277            NostrSigner::new(signer.clone(), self.config.network, account_number)
278                .await
279                .map_err(|e| SdkError::Generic(format!("{e:?}")))?,
280        );
281
282        let chain_service = if let Some(service) = self.chain_service {
283            service
284        } else {
285            let inner_client =
286                CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?;
287            match self.config.network {
288                Network::Mainnet => Arc::new(RestClientChainService::new(
289                    "https://blockstream.info/api".to_string(),
290                    self.config.network,
291                    5,
292                    Box::new(inner_client),
293                    None,
294                    ChainApiType::Esplora,
295                )),
296                Network::Regtest => Arc::new(RestClientChainService::new(
297                    "https://regtest-mempool.us-west-2.sparkinfra.net/api".to_string(),
298                    self.config.network,
299                    5,
300                    Box::new(inner_client),
301                    match (
302                        std::env::var("CHAIN_SERVICE_USERNAME"),
303                        std::env::var("CHAIN_SERVICE_PASSWORD"),
304                    ) {
305                        (Ok(username), Ok(password)) => Some(BasicAuth::new(username, password)),
306                        _ => Some(BasicAuth::new(
307                            "spark-sdk".to_string(),
308                            "mCMk1JqlBNtetUNy".to_string(),
309                        )),
310                    },
311                    ChainApiType::MempoolSpace,
312                )),
313            }
314        };
315
316        let (storage, sync_storage) = match (self.storage, self.storage_dir) {
317            // Use provided storages directly
318            (Some(storage), _) => (storage, self.sync_storage),
319            // Initialize default storages based on provided directory
320            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
321            (None, Some(storage_dir)) => {
322                let identity_pub_key = spark_signer
323                    .get_identity_public_key()
324                    .await
325                    .map_err(|e| SdkError::Generic(e.to_string()))?;
326                let storage =
327                    default_storage(&storage_dir, self.config.network, &identity_pub_key)?;
328                let sync_storage = match (self.sync_storage, &self.config.real_time_sync_server_url)
329                {
330                    // Use provided sync storage directly
331                    (Some(sync_storage), _) => Some(sync_storage),
332                    // Initialize default sync storage based on provided directory
333                    // if real-time sync is enabled
334                    (None, Some(_)) => Some(default_sync_storage(
335                        &storage_dir,
336                        self.config.network,
337                        &identity_pub_key,
338                    )?),
339                    _ => None,
340                };
341                (storage, sync_storage)
342            }
343            _ => {
344                return Err(SdkError::Generic(
345                    "Either storage or storage_dir must be set before building the SDK".to_string(),
346                ));
347            }
348        };
349
350        let fiat_service: Arc<dyn breez_sdk_common::fiat::FiatService> = match self.fiat_service {
351            Some(service) => Arc::new(FiatServiceWrapper::new(service)),
352            None => Arc::new(
353                BreezServer::new(PRODUCTION_BREEZSERVER_URL, None)
354                    .map_err(|e| SdkError::Generic(e.to_string()))?,
355            ),
356        };
357
358        let lnurl_client: Arc<dyn breez_sdk_common::rest::RestClient> = match self.lnurl_client {
359            Some(client) => Arc::new(RestClientWrapper::new(client)),
360            None => Arc::new(
361                CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?,
362            ),
363        };
364        let user_agent = format!(
365            "{}/{}",
366            crate::built_info::PKG_NAME,
367            crate::built_info::GIT_VERSION.unwrap_or(crate::built_info::PKG_VERSION),
368        );
369        info!("Building SparkWallet with user agent: {}", user_agent);
370        let mut spark_wallet_config =
371            spark_wallet::SparkWalletConfig::default_config(self.config.network.into());
372        spark_wallet_config.operator_pool = spark_wallet_config
373            .operator_pool
374            .with_user_agent(Some(user_agent.clone()));
375        spark_wallet_config.service_provider_config.user_agent = Some(user_agent);
376        spark_wallet_config.leaf_auto_optimize_enabled =
377            self.config.optimization_config.auto_enabled;
378        spark_wallet_config.leaf_optimization_options.multiplicity =
379            self.config.optimization_config.multiplicity;
380
381        let mut wallet_builder =
382            spark_wallet::WalletBuilder::new(spark_wallet_config, spark_signer);
383        if let Some(observer) = self.payment_observer {
384            let observer: Arc<dyn spark_wallet::TransferObserver> =
385                Arc::new(SparkTransferObserver::new(observer));
386            wallet_builder = wallet_builder.with_transfer_observer(observer);
387        }
388        let spark_wallet = Arc::new(wallet_builder.build().await?);
389
390        let lnurl_server_client: Option<Arc<dyn LnurlServerClient>> = match self.lnurl_server_client
391        {
392            Some(client) => Some(client),
393            None => match &self.config.lnurl_domain {
394                Some(domain) => {
395                    // Get the SparkWallet instance for signing
396                    Some(Arc::new(ReqwestLnurlServerClient::new(
397                        domain.clone(),
398                        self.config.api_key.clone(),
399                        Arc::clone(&spark_wallet),
400                    )?))
401                }
402                None => None,
403            },
404        };
405        let shutdown_sender = watch::channel::<()>(()).0;
406
407        let event_emitter = Arc::new(EventEmitter::new(
408            self.config.real_time_sync_server_url.is_some(),
409        ));
410        let storage = if let Some(server_url) = &self.config.real_time_sync_server_url {
411            let Some(sync_storage) = sync_storage else {
412                return Err(SdkError::Generic(
413                    "Real-time sync is enabled, but no sync storage is supplied".to_string(),
414                ));
415            };
416
417            init_and_start_real_time_sync(RealTimeSyncParams {
418                server_url: server_url.clone(),
419                api_key: self.config.api_key.clone(),
420                signer: rtsync_signer,
421                storage: Arc::clone(&storage),
422                sync_storage,
423                shutdown_receiver: shutdown_sender.subscribe(),
424                event_emitter: Arc::clone(&event_emitter),
425            })
426            .await?
427        } else {
428            storage
429        };
430        let flashnet_client = Arc::new(FlashnetClient::new(
431            FlashnetConfig::default_config(self.config.network.into()),
432            spark_wallet.clone(),
433            Arc::new(CacheStore::default()),
434        ));
435
436        let nostr_client = Arc::new(NostrClient::new(nostr_signer));
437
438        // Create the SDK instance
439        let sdk = BreezSdk::init_and_start(BreezSdkParams {
440            config: self.config,
441            storage,
442            chain_service,
443            fiat_service,
444            lnurl_client,
445            lnurl_server_client,
446            shutdown_sender,
447            spark_wallet,
448            event_emitter,
449            nostr_client,
450            flashnet_client,
451        })?;
452        debug!("Initialized and started breez sdk.");
453
454        Ok(sdk)
455    }
456}
457
458#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
459fn default_storage(
460    data_dir: &str,
461    network: Network,
462    identity_pub_key: &spark_wallet::PublicKey,
463) -> Result<Arc<dyn Storage>, SdkError> {
464    let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
465    let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
466    Ok(storage)
467}
468
469#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
470fn default_sync_storage(
471    data_dir: &str,
472    network: Network,
473    identity_pub_key: &spark_wallet::PublicKey,
474) -> Result<Arc<dyn SyncStorage>, SdkError> {
475    let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
476    let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
477    Ok(storage)
478}