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#[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#[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 #[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 #[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 #[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 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 pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
176 self.storage = Some(storage);
177 self
178 }
179
180 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[allow(clippy::too_many_lines)]
296 pub async fn build(self) -> Result<BreezSdk, SdkError> {
297 self.config.validate()?;
299
300 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 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 #[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 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 #[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 let buy_bitcoin_provider: Arc<dyn BuyBitcoinProviderApi> =
534 Arc::new(MoonpayProvider::new(breez_server.clone()));
535
536 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}