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