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::moonpay::MoonpayProvider,
10};
11use platform_utils::DefaultHttpClient;
12
13#[cfg(not(target_family = "wasm"))]
14use spark_wallet::Signer;
15use spark_wallet::{SparkWalletConfig, TokenOutputStore, TreeStore};
16use tokio::sync::watch;
17use tracing::{debug, info};
18
19use flashnet::{FlashnetConfig, IntegratorConfig};
20
21use crate::{
22 Credentials, EventEmitter, FiatService, FiatServiceWrapper, KeySetType, Network, Seed,
23 chain::{
24 BitcoinChainService,
25 rest_client::{BasicAuth, ChainApiType, RestClientChainService},
26 },
27 error::SdkError,
28 lnurl::{DefaultLnurlServerClient, LnurlServerClient},
29 models::Config,
30 payment_observer::{PaymentObserver, SparkTransferObserver},
31 persist::Storage,
32 realtime_sync::{RealTimeSyncParams, init_and_start_real_time_sync},
33 sdk::{BreezSdk, BreezSdkParams, SyncCoordinator},
34 signer::{
35 breez::BreezSignerImpl, lnurl_auth::LnurlAuthSignerAdapter, rtsync::RTSyncSigner,
36 spark::SparkSigner,
37 },
38 stable_balance::StableBalance,
39 token_conversion::TokenConversionMiddleware,
40 token_conversion::{
41 DEFAULT_INTEGRATOR_FEE_BPS, DEFAULT_INTEGRATOR_PUBKEY, FlashnetTokenConverter,
42 TokenConverter,
43 },
44};
45
46#[derive(Clone)]
48enum SignerSource {
49 Seed {
50 seed: Seed,
51 key_set_type: KeySetType,
52 use_address_index: bool,
53 account_number: Option<u32>,
54 },
55 External(Arc<dyn crate::signer::ExternalSigner>),
56}
57
58#[derive(Clone)]
60pub struct SdkBuilder {
61 config: Config,
62 signer_source: SignerSource,
63
64 storage_dir: Option<String>,
65 storage: Option<Arc<dyn Storage>>,
66 #[cfg(feature = "postgres")]
67 postgres_backend_config: Option<crate::persist::postgres::PostgresStorageConfig>,
68 chain_service: Option<Arc<dyn BitcoinChainService>>,
69 fiat_service: Option<Arc<dyn FiatService>>,
70 lnurl_client: Option<Arc<dyn platform_utils::HttpClient>>,
71 lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
72 payment_observer: Option<Arc<dyn PaymentObserver>>,
73 tree_store: Option<Arc<dyn TreeStore>>,
74 token_output_store: Option<Arc<dyn TokenOutputStore>>,
75}
76
77impl SdkBuilder {
78 #[allow(clippy::needless_pass_by_value)]
86 pub fn new(config: Config, seed: Seed) -> Self {
87 SdkBuilder {
88 config,
89 signer_source: SignerSource::Seed {
90 seed,
91 key_set_type: KeySetType::Default,
92 use_address_index: false,
93 account_number: None,
94 },
95 storage_dir: None,
96 storage: None,
97 #[cfg(feature = "postgres")]
98 postgres_backend_config: None,
99 chain_service: None,
100 fiat_service: None,
101 lnurl_client: None,
102 lnurl_server_client: None,
103 payment_observer: None,
104 tree_store: None,
105 token_output_store: 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(feature = "postgres")]
122 postgres_backend_config: None,
123 chain_service: None,
124 fiat_service: None,
125 lnurl_client: None,
126 lnurl_server_client: None,
127 payment_observer: None,
128 tree_store: None,
129 token_output_store: None,
130 }
131 }
132
133 #[must_use]
141 pub fn with_key_set(mut self, config: crate::models::KeySetConfig) -> Self {
142 if let SignerSource::Seed {
143 key_set_type: ref mut kst,
144 use_address_index: ref mut uai,
145 account_number: ref mut an,
146 ..
147 } = self.signer_source
148 {
149 *kst = config.key_set_type;
150 *uai = config.use_address_index;
151 *an = config.account_number;
152 }
153 self
154 }
155
156 #[must_use]
157 pub fn with_default_storage(mut self, storage_dir: String) -> Self {
163 self.storage_dir = Some(storage_dir);
164 self
165 }
166
167 #[must_use]
168 pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
172 self.storage = Some(storage);
173 self
174 }
175
176 #[must_use]
181 #[cfg(feature = "postgres")]
182 pub fn with_postgres_backend(
183 mut self,
184 config: crate::persist::postgres::PostgresStorageConfig,
185 ) -> Self {
186 self.postgres_backend_config = Some(config);
187 self
188 }
189
190 #[must_use]
194 pub fn with_chain_service(mut self, chain_service: Arc<dyn BitcoinChainService>) -> Self {
195 self.chain_service = Some(chain_service);
196 self
197 }
198
199 #[must_use]
205 pub fn with_rest_chain_service(
206 mut self,
207 url: String,
208 api_type: ChainApiType,
209 credentials: Option<Credentials>,
210 ) -> Self {
211 self.chain_service = Some(Arc::new(RestClientChainService::new(
212 url,
213 self.config.network,
214 5,
215 Box::new(DefaultHttpClient::default()),
216 credentials.map(|c| BasicAuth::new(c.username, c.password)),
217 api_type,
218 )));
219 self
220 }
221
222 #[must_use]
226 pub fn with_fiat_service(mut self, fiat_service: Arc<dyn FiatService>) -> Self {
227 self.fiat_service = Some(fiat_service);
228 self
229 }
230
231 #[must_use]
232 pub fn with_lnurl_client(mut self, lnurl_client: Arc<dyn crate::RestClient>) -> Self {
233 self.lnurl_client = Some(Arc::new(crate::common::rest::RestClientWrapper::new(
234 lnurl_client,
235 )));
236 self
237 }
238
239 #[must_use]
240 #[allow(unused)]
241 pub fn with_lnurl_server_client(
242 mut self,
243 lnurl_serverclient: Arc<dyn LnurlServerClient>,
244 ) -> Self {
245 self.lnurl_server_client = Some(lnurl_serverclient);
246 self
247 }
248
249 #[must_use]
254 #[allow(unused)]
255 pub fn with_payment_observer(mut self, payment_observer: Arc<dyn PaymentObserver>) -> Self {
256 self.payment_observer = Some(payment_observer);
257 self
258 }
259
260 #[must_use]
265 pub fn with_tree_store(mut self, tree_store: Arc<dyn TreeStore>) -> Self {
266 self.tree_store = Some(tree_store);
267 self
268 }
269
270 #[must_use]
275 pub fn with_token_output_store(
276 mut self,
277 token_output_store: Arc<dyn TokenOutputStore>,
278 ) -> Self {
279 self.token_output_store = Some(token_output_store);
280 self
281 }
282
283 fn build_spark_wallet_config(
286 network: spark_wallet::Network,
287 env_config: &crate::models::SparkConfig,
288 ) -> Result<spark_wallet::SparkWalletConfig, SdkError> {
289 let coordinator_index = env_config
290 .signing_operators
291 .iter()
292 .position(|op| op.identifier == env_config.coordinator_identifier)
293 .ok_or_else(|| {
294 SdkError::InvalidInput(
295 "coordinator_identifier does not match any signing operator".to_string(),
296 )
297 })?;
298
299 let operators: Vec<_> = env_config
300 .signing_operators
301 .iter()
302 .map(|op| {
303 SparkWalletConfig::create_operator_config(
304 op.id as usize,
305 &op.identifier,
306 &op.address,
307 None,
308 &op.identity_public_key,
309 )
310 .map_err(|e| SdkError::InvalidInput(e.to_string()))
311 })
312 .collect::<Result<_, _>>()?;
313
314 let operator_pool = spark_wallet::OperatorPoolConfig::new(coordinator_index, operators)
315 .map_err(|e| SdkError::InvalidInput(e.to_string()))?;
316
317 let service_provider_config = SparkWalletConfig::create_service_provider_config(
318 &env_config.ssp_config.base_url,
319 &env_config.ssp_config.identity_public_key,
320 env_config.ssp_config.schema_endpoint.clone(),
321 )
322 .map_err(|e| SdkError::InvalidInput(e.to_string()))?;
323
324 let mut config = SparkWalletConfig::default_config(network);
325 config.operator_pool = operator_pool;
326 config.split_secret_threshold = env_config.threshold;
327 config.service_provider_config = service_provider_config;
328 config.tokens_config.expected_withdraw_bond_sats = env_config.expected_withdraw_bond_sats;
329 config
330 .tokens_config
331 .expected_withdraw_relative_block_locktime =
332 env_config.expected_withdraw_relative_block_locktime;
333
334 Ok(config)
335 }
336
337 #[allow(clippy::too_many_lines)]
339 pub async fn build(self) -> Result<BreezSdk, SdkError> {
340 self.config.validate()?;
342
343 let signer: Arc<dyn crate::signer::BreezSigner> = match self.signer_source {
345 SignerSource::Seed {
346 seed,
347 key_set_type,
348 use_address_index,
349 account_number,
350 } => Arc::new(
351 BreezSignerImpl::new(
352 &self.config,
353 &seed,
354 key_set_type.into(),
355 use_address_index,
356 account_number,
357 )
358 .map_err(|e| SdkError::Generic(e.to_string()))?,
359 ),
360 SignerSource::External(external_signer) => {
361 use crate::signer::ExternalSignerAdapter;
362 Arc::new(ExternalSignerAdapter::new(external_signer))
363 }
364 };
365
366 let spark_signer = Arc::new(SparkSigner::new(signer.clone()));
368 let rtsync_signer = Arc::new(
369 RTSyncSigner::new(signer.clone(), self.config.network)
370 .map_err(|e| SdkError::Generic(e.to_string()))?,
371 );
372 let lnurl_auth_signer = Arc::new(LnurlAuthSignerAdapter::new(signer.clone()));
373
374 let chain_service = if let Some(service) = self.chain_service {
375 service
376 } else {
377 let inner_client = DefaultHttpClient::default();
378 match self.config.network {
379 Network::Mainnet => Arc::new(RestClientChainService::new(
380 "https://blockstream.info/api".to_string(),
381 self.config.network,
382 5,
383 Box::new(inner_client),
384 None,
385 ChainApiType::Esplora,
386 )),
387 Network::Regtest => Arc::new(RestClientChainService::new(
388 "https://regtest-mempool.us-west-2.sparkinfra.net/api".to_string(),
389 self.config.network,
390 5,
391 Box::new(inner_client),
392 match (
393 std::env::var("CHAIN_SERVICE_USERNAME"),
394 std::env::var("CHAIN_SERVICE_PASSWORD"),
395 ) {
396 (Ok(username), Ok(password)) => Some(BasicAuth::new(username, password)),
397 _ => Some(BasicAuth::new(
398 "spark-sdk".to_string(),
399 "mCMk1JqlBNtetUNy".to_string(),
400 )),
401 },
402 ChainApiType::MempoolSpace,
403 )),
404 }
405 };
406
407 #[cfg(feature = "postgres")]
409 let has_postgres = self.postgres_backend_config.is_some();
410 #[cfg(not(feature = "postgres"))]
411 let has_postgres = false;
412
413 let storage_count = [
414 self.storage.is_some(),
415 self.storage_dir.is_some(),
416 has_postgres,
417 ]
418 .into_iter()
419 .filter(|&v| v)
420 .count();
421 match storage_count {
422 0 => return Err(SdkError::Generic("No storage configured".to_string())),
423 2.. => {
424 return Err(SdkError::Generic(
425 "Multiple storage configurations provided".to_string(),
426 ));
427 }
428 _ => {}
429 }
430
431 #[cfg(feature = "postgres")]
434 let postgres_pool = if let Some(ref postgres_config) = self.postgres_backend_config {
435 Some(
436 crate::persist::postgres::create_pool(postgres_config)
437 .map_err(|e| SdkError::Generic(e.to_string()))?,
438 )
439 } else {
440 None
441 };
442
443 let storage: Arc<dyn Storage> = if let Some(storage) = self.storage {
445 storage
446 } else if let Some(storage_dir) = self.storage_dir {
447 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
448 {
449 let identity_pub_key = spark_signer
450 .get_identity_public_key()
451 .await
452 .map_err(|e| SdkError::Generic(e.to_string()))?;
453 default_storage(&storage_dir, self.config.network, &identity_pub_key)?
454 }
455 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
456 {
457 let _ = storage_dir;
458 return Err(SdkError::Generic(
459 "with_default_storage is not supported on WASM".to_string(),
460 ));
461 }
462 } else {
463 #[cfg(all(
464 feature = "postgres",
465 not(all(target_family = "wasm", target_os = "unknown"))
466 ))]
467 if let Some(ref pool) = postgres_pool {
468 Arc::new(
469 crate::persist::postgres::PostgresStorage::new_with_pool(pool.clone())
470 .await
471 .map_err(|e| SdkError::Generic(e.to_string()))?,
472 )
473 } else {
474 return Err(SdkError::Generic("No storage configured".to_string()));
475 }
476 #[cfg(not(all(
477 feature = "postgres",
478 not(all(target_family = "wasm", target_os = "unknown"))
479 )))]
480 {
481 return Err(SdkError::Generic("No storage configured".to_string()));
482 }
483 };
484
485 let user_agent = format!(
486 "{}/{}",
487 crate::built_info::PKG_NAME,
488 crate::built_info::GIT_VERSION.unwrap_or(crate::built_info::PKG_VERSION),
489 );
490 info!("Building sdk with user agent: {}", user_agent);
491
492 let breez_server = Arc::new(
493 BreezServer::new(PRODUCTION_BREEZSERVER_URL, None, &user_agent)
494 .map_err(|e| SdkError::Generic(e.to_string()))?,
495 );
496
497 let fiat_service: Arc<dyn breez_sdk_common::fiat::FiatService> = match self.fiat_service {
498 Some(service) => Arc::new(FiatServiceWrapper::new(service)),
499 None => breez_server.clone(),
500 };
501
502 let lnurl_client: Arc<dyn platform_utils::HttpClient> = match self.lnurl_client {
503 Some(client) => client,
504 None => Arc::new(DefaultHttpClient::default()),
505 };
506 let mut spark_wallet_config = if let Some(env_config) = &self.config.spark_config {
507 Self::build_spark_wallet_config(self.config.network.into(), env_config)?
508 } else {
509 spark_wallet::SparkWalletConfig::default_config(self.config.network.into())
510 };
511 spark_wallet_config.operator_pool = spark_wallet_config
512 .operator_pool
513 .with_user_agent(Some(user_agent.clone()));
514 spark_wallet_config.service_provider_config.user_agent = Some(user_agent.clone());
515 spark_wallet_config.leaf_auto_optimize_enabled =
516 self.config.optimization_config.auto_enabled;
517 spark_wallet_config.leaf_optimization_options.multiplicity =
518 self.config.optimization_config.multiplicity;
519 spark_wallet_config.max_concurrent_claims = self.config.max_concurrent_claims;
520
521 let shutdown_sender = watch::channel::<()>(()).0;
522
523 #[allow(unused_mut)]
525 let mut tree_store: Option<Arc<dyn TreeStore>> = self.tree_store;
526
527 #[cfg(feature = "postgres")]
528 if tree_store.is_none()
529 && let Some(ref pool) = postgres_pool
530 {
531 tree_store =
532 Some(crate::persist::postgres::create_postgres_tree_store(pool.clone()).await?);
533 }
534
535 #[allow(unused_mut)]
537 let mut token_output_store: Option<Arc<dyn TokenOutputStore>> = self.token_output_store;
538
539 #[cfg(feature = "postgres")]
540 if token_output_store.is_none()
541 && let Some(ref pool) = postgres_pool
542 {
543 token_output_store =
544 Some(crate::persist::postgres::create_postgres_token_store(pool.clone()).await?);
545 }
546
547 let mut wallet_builder =
548 spark_wallet::WalletBuilder::new(spark_wallet_config, spark_signer)
549 .with_cancellation_token(shutdown_sender.subscribe());
550 if let Some(observer) = self.payment_observer {
551 let observer: Arc<dyn spark_wallet::TransferObserver> =
552 Arc::new(SparkTransferObserver::new(observer));
553 wallet_builder = wallet_builder.with_transfer_observer(observer);
554 }
555 if let Some(tree_store) = tree_store {
556 wallet_builder = wallet_builder.with_tree_store(tree_store);
557 }
558 if let Some(token_output_store) = token_output_store {
559 wallet_builder = wallet_builder.with_token_output_store(token_output_store);
560 }
561 let spark_wallet = Arc::new(wallet_builder.build().await?);
562
563 let lnurl_server_client: Option<Arc<dyn LnurlServerClient>> = match self.lnurl_server_client
564 {
565 Some(client) => Some(client),
566 None => match &self.config.lnurl_domain {
567 Some(domain) => {
568 let http_client: Arc<dyn platform_utils::HttpClient> =
569 Arc::new(DefaultHttpClient::default());
570 Some(Arc::new(DefaultLnurlServerClient::new(
571 http_client,
572 domain.clone(),
573 self.config.api_key.clone(),
574 Arc::clone(&spark_wallet),
575 )))
576 }
577 None => None,
578 },
579 };
580
581 let event_emitter = Arc::new(EventEmitter::new(
582 self.config.real_time_sync_server_url.is_some(),
583 ));
584
585 let storage = if let Some(server_url) = &self.config.real_time_sync_server_url {
586 init_and_start_real_time_sync(RealTimeSyncParams {
587 server_url: server_url.clone(),
588 api_key: self.config.api_key.clone(),
589 user_agent,
590 signer: rtsync_signer,
591 storage: Arc::clone(&storage),
592 shutdown_receiver: shutdown_sender.subscribe(),
593 event_emitter: Arc::clone(&event_emitter),
594 lnurl_server_client: lnurl_server_client.clone(),
595 })
596 .await?
597 } else {
598 storage
599 };
600
601 let buy_bitcoin_provider = Arc::new(MoonpayProvider::new(breez_server.clone()));
603
604 let flashnet_config = FlashnetConfig::default_config(
606 self.config.network.into(),
607 DEFAULT_INTEGRATOR_PUBKEY
608 .parse()
609 .ok()
610 .map(|pubkey| IntegratorConfig {
611 pubkey,
612 fee_bps: DEFAULT_INTEGRATOR_FEE_BPS,
613 }),
614 );
615 let token_converter: Arc<dyn TokenConverter> = Arc::new(FlashnetTokenConverter::new(
616 flashnet_config,
617 Arc::clone(&storage),
618 Arc::clone(&spark_wallet),
619 self.config.network,
620 shutdown_sender.subscribe(),
621 ));
622
623 let sync_coordinator = SyncCoordinator::new();
625
626 let stable_balance = if let Some(config) = &self.config.stable_balance_config {
630 Some(Arc::new(
631 StableBalance::new(
632 config.clone(),
633 Arc::clone(&token_converter),
634 Arc::clone(&spark_wallet),
635 Arc::clone(&storage),
636 shutdown_sender.subscribe(),
637 Arc::clone(&event_emitter),
638 sync_coordinator.clone(),
639 )
640 .await,
641 ))
642 } else {
643 None
644 };
645
646 event_emitter
649 .add_middleware(Box::new(TokenConversionMiddleware))
650 .await;
651
652 let sdk = BreezSdk::init_and_start(BreezSdkParams {
654 config: self.config,
655 storage,
656 chain_service,
657 fiat_service,
658 lnurl_client,
659 lnurl_server_client,
660 lnurl_auth_signer,
661 shutdown_sender,
662 spark_wallet,
663 event_emitter,
664 buy_bitcoin_provider,
665 token_converter,
666 stable_balance,
667 sync_coordinator,
668 })?;
669 debug!("Initialized and started breez sdk.");
670
671 Ok(sdk)
672 }
673}
674
675#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
676fn default_storage(
677 data_dir: &str,
678 network: Network,
679 identity_pub_key: &spark_wallet::PublicKey,
680) -> Result<Arc<dyn Storage>, SdkError> {
681 let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
682 let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
683 Ok(storage)
684}