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