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