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::buy::moonpay::MoonpayProvider;
8
9#[cfg(not(target_family = "wasm"))]
10use spark_wallet::Signer;
11use spark_wallet::{InMemorySessionManager, SparkWalletConfig, TokenOutputStore, TreeStore};
12use tokio::sync::watch;
13use tracing::{debug, info};
14
15use flashnet::{FlashnetConfig, IntegratorConfig};
16
17use crate::{
18 Credentials, EventEmitter, FiatService, FiatServiceWrapper, KeySetType, Network, Seed,
19 chain::{
20 BitcoinChainService,
21 rest_client::{BasicAuth, ChainApiType, RestClientChainService},
22 },
23 error::SdkError,
24 lnurl::{DefaultLnurlServerClient, LnurlServerClient},
25 models::Config,
26 payment_observer::{PaymentObserver, SparkTransferObserver},
27 persist::Storage,
28 realtime_sync::{RealTimeSyncParams, init_and_start_real_time_sync},
29 sdk::{BreezSdk, BreezSdkParams, SyncCoordinator, runtime_from_config},
30 sdk_context::{SdkContext, SdkContextConfig, new_shared_sdk_context},
31 session_manager::{SessionManager, SessionManagerAdapter},
32 signer::{
33 breez::BreezSignerImpl, lnurl_auth::LnurlAuthSignerAdapter, rtsync::RTSyncSigner,
34 spark::SparkSigner,
35 },
36 stable_balance::StableBalance,
37 token_conversion::TokenConversionMiddleware,
38 token_conversion::{
39 DEFAULT_INTEGRATOR_FEE_BPS, DEFAULT_INTEGRATOR_PUBKEY, FlashnetTokenConverter,
40 TokenConverter,
41 },
42};
43
44#[derive(Clone)]
50struct RestChainServiceConfig {
51 url: String,
52 api_type: ChainApiType,
53 credentials: Option<Credentials>,
54}
55
56#[derive(Clone)]
58enum SignerSource {
59 Seed {
60 seed: Seed,
61 key_set_type: KeySetType,
62 use_address_index: bool,
63 account_number: Option<u32>,
64 },
65 External(Arc<dyn crate::signer::ExternalSigner>),
66}
67
68#[derive(Clone)]
70pub struct SdkBuilder {
71 config: Config,
72 signer_source: SignerSource,
73
74 storage_dir: Option<String>,
75 storage: Option<Arc<dyn Storage>>,
76 #[cfg(feature = "postgres")]
77 postgres_pool: Option<Arc<crate::persist::postgres::PostgresConnectionPool>>,
78 #[cfg(feature = "mysql")]
79 mysql_pool: Option<Arc<crate::persist::mysql::MysqlConnectionPool>>,
80 chain_service: Option<Arc<dyn BitcoinChainService>>,
81 rest_chain_service_config: Option<RestChainServiceConfig>,
82 fiat_service: Option<Arc<dyn FiatService>>,
83 lnurl_client: Option<Arc<dyn platform_utils::HttpClient>>,
84 lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
85 payment_observer: Option<Arc<dyn PaymentObserver>>,
86 tree_store: Option<Arc<dyn TreeStore>>,
87 token_output_store: Option<Arc<dyn TokenOutputStore>>,
88 context: Option<Arc<SdkContext>>,
89 session_manager: Option<Arc<dyn SessionManager>>,
90}
91
92impl SdkBuilder {
93 #[allow(clippy::needless_pass_by_value)]
101 pub fn new(config: Config, seed: Seed) -> Self {
102 SdkBuilder {
103 config,
104 signer_source: SignerSource::Seed {
105 seed,
106 key_set_type: KeySetType::Default,
107 use_address_index: false,
108 account_number: None,
109 },
110 storage_dir: None,
111 storage: None,
112 #[cfg(feature = "postgres")]
113 postgres_pool: None,
114 #[cfg(feature = "mysql")]
115 mysql_pool: None,
116 chain_service: None,
117 rest_chain_service_config: None,
118 fiat_service: None,
119 lnurl_client: None,
120 lnurl_server_client: None,
121 payment_observer: None,
122 tree_store: None,
123 token_output_store: None,
124 context: None,
125 session_manager: None,
126 }
127 }
128
129 #[allow(clippy::needless_pass_by_value)]
135 pub fn new_with_signer(config: Config, signer: Arc<dyn crate::signer::ExternalSigner>) -> Self {
136 SdkBuilder {
137 config,
138 signer_source: SignerSource::External(signer),
139 storage_dir: None,
140 storage: None,
141 #[cfg(feature = "postgres")]
142 postgres_pool: None,
143 #[cfg(feature = "mysql")]
144 mysql_pool: None,
145 chain_service: None,
146 rest_chain_service_config: None,
147 fiat_service: None,
148 lnurl_client: None,
149 lnurl_server_client: None,
150 payment_observer: None,
151 tree_store: None,
152 token_output_store: None,
153 context: None,
154 session_manager: None,
155 }
156 }
157
158 #[must_use]
166 pub fn with_key_set(mut self, config: crate::models::KeySetConfig) -> Self {
167 if let SignerSource::Seed {
168 key_set_type: ref mut kst,
169 use_address_index: ref mut uai,
170 account_number: ref mut an,
171 ..
172 } = self.signer_source
173 {
174 *kst = config.key_set_type;
175 *uai = config.use_address_index;
176 *an = config.account_number;
177 }
178 self
179 }
180
181 #[must_use]
182 pub fn with_default_storage(mut self, storage_dir: String) -> Self {
188 self.storage_dir = Some(storage_dir);
189 self
190 }
191
192 #[must_use]
193 pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
197 self.storage = Some(storage);
198 self
199 }
200
201 #[must_use]
211 pub fn with_shared_context(mut self, context: Arc<SdkContext>) -> Self {
212 self.context = Some(context);
213 self
214 }
215
216 #[must_use]
228 #[cfg(feature = "postgres")]
229 pub fn with_postgres_connection_pool(
230 mut self,
231 pool: Arc<crate::persist::postgres::PostgresConnectionPool>,
232 ) -> Self {
233 self.postgres_pool = Some(pool);
234 self
235 }
236
237 #[must_use]
249 #[cfg(feature = "mysql")]
250 pub fn with_mysql_connection_pool(
251 mut self,
252 pool: Arc<crate::persist::mysql::MysqlConnectionPool>,
253 ) -> Self {
254 self.mysql_pool = Some(pool);
255 self
256 }
257
258 #[cfg(feature = "postgres")]
264 #[deprecated(
265 note = "Call `create_postgres_connection_pool(&config)` and `with_postgres_connection_pool(pool)` instead."
266 )]
267 #[allow(clippy::needless_pass_by_value)]
268 pub fn with_postgres_backend(
269 self,
270 config: crate::persist::postgres::PostgresStorageConfig,
271 ) -> Result<Self, SdkError> {
272 let pool = crate::persist::postgres::create_postgres_connection_pool(&config)?;
273 Ok(self.with_postgres_connection_pool(pool))
274 }
275
276 #[cfg(feature = "mysql")]
282 #[deprecated(
283 note = "Call `create_mysql_connection_pool(&config)` and `with_mysql_connection_pool(pool)` instead."
284 )]
285 #[allow(clippy::needless_pass_by_value)]
286 pub fn with_mysql_backend(
287 self,
288 config: crate::persist::mysql::MysqlStorageConfig,
289 ) -> Result<Self, SdkError> {
290 let pool = crate::persist::mysql::create_mysql_connection_pool(&config)?;
291 Ok(self.with_mysql_connection_pool(pool))
292 }
293
294 #[cfg(target_family = "wasm")]
299 #[must_use]
300 pub fn with_session_manager(mut self, session_manager: Arc<dyn SessionManager>) -> Self {
301 self.session_manager = Some(session_manager);
302 self
303 }
304
305 #[must_use]
309 pub fn with_chain_service(mut self, chain_service: Arc<dyn BitcoinChainService>) -> Self {
310 self.chain_service = Some(chain_service);
311 self.rest_chain_service_config = None;
312 self
313 }
314
315 #[must_use]
325 pub fn with_rest_chain_service(
326 mut self,
327 url: String,
328 api_type: ChainApiType,
329 credentials: Option<Credentials>,
330 ) -> Self {
331 self.chain_service = None;
332 self.rest_chain_service_config = Some(RestChainServiceConfig {
333 url,
334 api_type,
335 credentials,
336 });
337 self
338 }
339
340 #[must_use]
344 pub fn with_fiat_service(mut self, fiat_service: Arc<dyn FiatService>) -> Self {
345 self.fiat_service = Some(fiat_service);
346 self
347 }
348
349 #[must_use]
350 pub fn with_lnurl_client(mut self, lnurl_client: Arc<dyn crate::RestClient>) -> Self {
351 self.lnurl_client = Some(Arc::new(crate::common::rest::RestClientWrapper::new(
352 lnurl_client,
353 )));
354 self
355 }
356
357 #[must_use]
358 #[allow(unused)]
359 pub fn with_lnurl_server_client(
360 mut self,
361 lnurl_serverclient: Arc<dyn LnurlServerClient>,
362 ) -> Self {
363 self.lnurl_server_client = Some(lnurl_serverclient);
364 self
365 }
366
367 #[must_use]
372 #[allow(unused)]
373 pub fn with_payment_observer(mut self, payment_observer: Arc<dyn PaymentObserver>) -> Self {
374 self.payment_observer = Some(payment_observer);
375 self
376 }
377
378 #[must_use]
383 pub fn with_tree_store(mut self, tree_store: Arc<dyn TreeStore>) -> Self {
384 self.tree_store = Some(tree_store);
385 self
386 }
387
388 #[must_use]
393 pub fn with_token_output_store(
394 mut self,
395 token_output_store: Arc<dyn TokenOutputStore>,
396 ) -> Self {
397 self.token_output_store = Some(token_output_store);
398 self
399 }
400
401 fn build_spark_wallet_config(
404 network: spark_wallet::Network,
405 env_config: &crate::models::SparkConfig,
406 ) -> Result<spark_wallet::SparkWalletConfig, SdkError> {
407 let coordinator_index = env_config
408 .signing_operators
409 .iter()
410 .position(|op| op.identifier == env_config.coordinator_identifier)
411 .ok_or_else(|| {
412 SdkError::InvalidInput(
413 "coordinator_identifier does not match any signing operator".to_string(),
414 )
415 })?;
416
417 let operators: Vec<_> = env_config
418 .signing_operators
419 .iter()
420 .map(|op| {
421 SparkWalletConfig::create_operator_config(
422 op.id as usize,
423 &op.identifier,
424 &op.address,
425 None,
426 &op.identity_public_key,
427 )
428 .map_err(|e| SdkError::InvalidInput(e.to_string()))
429 })
430 .collect::<Result<_, _>>()?;
431
432 let operator_pool = spark_wallet::OperatorPoolConfig::new(coordinator_index, operators)
433 .map_err(|e| SdkError::InvalidInput(e.to_string()))?;
434
435 let service_provider_config = SparkWalletConfig::create_service_provider_config(
436 &env_config.ssp_config.base_url,
437 &env_config.ssp_config.identity_public_key,
438 env_config.ssp_config.schema_endpoint.clone(),
439 )
440 .map_err(|e| SdkError::InvalidInput(e.to_string()))?;
441
442 let mut config = SparkWalletConfig::default_config(network);
443 config.operator_pool = operator_pool;
444 config.split_secret_threshold = env_config.threshold;
445 config.service_provider_config = service_provider_config;
446 config.tokens_config.expected_withdraw_bond_sats = env_config.expected_withdraw_bond_sats;
447 config
448 .tokens_config
449 .expected_withdraw_relative_block_locktime =
450 env_config.expected_withdraw_relative_block_locktime;
451
452 Ok(config)
453 }
454
455 #[allow(clippy::too_many_lines)]
457 pub async fn build(self) -> Result<BreezSdk, SdkError> {
458 self.config.validate()?;
460 let runtime = runtime_from_config(&self.config);
461 if !runtime.starts_background_services() {
462 if self.config.stable_balance_config.is_some() {
463 return Err(SdkError::InvalidInput(
464 "stable_balance_config is not supported when background_tasks_enabled is false"
465 .to_string(),
466 ));
467 }
468 if self.config.real_time_sync_server_url.is_some() {
469 return Err(SdkError::InvalidInput(
470 "real_time_sync_server_url must be None when background_tasks_enabled is false"
471 .to_string(),
472 ));
473 }
474 if self.config.leaf_optimization_config.auto_enabled {
475 return Err(SdkError::InvalidInput(
476 "leaf_optimization_config.auto_enabled must be false when background_tasks_enabled is false"
477 .to_string(),
478 ));
479 }
480 if self.config.token_optimization_config.auto_enabled {
481 return Err(SdkError::InvalidInput(
482 "token_optimization_config.auto_enabled must be false when background_tasks_enabled is false"
483 .to_string(),
484 ));
485 }
486 }
487
488 let signer: Arc<dyn crate::signer::BreezSigner> = match self.signer_source {
490 SignerSource::Seed {
491 seed,
492 key_set_type,
493 use_address_index,
494 account_number,
495 } => Arc::new(
496 BreezSignerImpl::new(
497 &self.config,
498 &seed,
499 key_set_type.into(),
500 use_address_index,
501 account_number,
502 )
503 .map_err(|e| SdkError::Generic(e.to_string()))?,
504 ),
505 SignerSource::External(external_signer) => {
506 use crate::signer::ExternalSignerAdapter;
507 Arc::new(ExternalSignerAdapter::new(external_signer))
508 }
509 };
510
511 let spark_signer = Arc::new(SparkSigner::new(signer.clone()));
513 let rtsync_signer = Arc::new(
514 RTSyncSigner::new(signer.clone(), self.config.network)
515 .map_err(|e| SdkError::Generic(e.to_string()))?,
516 );
517 let lnurl_auth_signer = Arc::new(LnurlAuthSignerAdapter::new(signer.clone()));
518
519 let context = match self.context {
523 Some(ctx) => ctx,
524 None => {
525 new_shared_sdk_context(SdkContextConfig {
526 api_key: self.config.api_key.clone(),
527 ..SdkContextConfig::new(self.config.network)
528 })
529 .await?
530 }
531 };
532
533 if context.network != self.config.network || context.api_key != self.config.api_key {
535 return Err(SdkError::Generic(
536 "SdkContext network/api_key do not match SdkConfig".to_string(),
537 ));
538 }
539
540 #[cfg(feature = "postgres")]
545 let postgres_pool: Option<Arc<crate::persist::postgres::PostgresConnectionPool>> = match (
546 self.postgres_pool,
547 context.postgres_pool.clone(),
548 ) {
549 (Some(_), Some(_)) => {
550 return Err(SdkError::Generic(
551 "multiple postgres pools configured: passed both via with_postgres_connection_pool and SdkContext"
552 .to_string(),
553 ));
554 }
555 (Some(p), None) | (None, Some(p)) => Some(p),
556 (None, None) => None,
557 };
558 #[cfg(feature = "mysql")]
559 let mysql_pool: Option<Arc<crate::persist::mysql::MysqlConnectionPool>> = match (
560 self.mysql_pool,
561 context.mysql_pool.clone(),
562 ) {
563 (Some(_), Some(_)) => {
564 return Err(SdkError::Generic(
565 "multiple mysql pools configured: passed both via with_mysql_connection_pool and SdkContext"
566 .to_string(),
567 ));
568 }
569 (Some(p), None) | (None, Some(p)) => Some(p),
570 (None, None) => None,
571 };
572
573 let chain_service: Arc<dyn BitcoinChainService> = if let Some(service) = self.chain_service
574 {
575 service
576 } else if let Some(cfg) = self.rest_chain_service_config {
577 Arc::new(RestClientChainService::new(
578 cfg.url,
579 self.config.network,
580 5,
581 context.http_client.clone(),
582 cfg.credentials
583 .map(|c| BasicAuth::new(c.username, c.password)),
584 cfg.api_type,
585 ))
586 } else {
587 let inner_client: Arc<dyn platform_utils::HttpClient> = context.http_client.clone();
588 match self.config.network {
589 Network::Mainnet => Arc::new(RestClientChainService::new(
590 "https://blockstream.info/api".to_string(),
591 self.config.network,
592 5,
593 inner_client,
594 None,
595 ChainApiType::Esplora,
596 )),
597 Network::Regtest => Arc::new(RestClientChainService::new(
598 "https://regtest-mempool.us-west-2.sparkinfra.net/api".to_string(),
599 self.config.network,
600 5,
601 inner_client,
602 match (
603 std::env::var("CHAIN_SERVICE_USERNAME"),
604 std::env::var("CHAIN_SERVICE_PASSWORD"),
605 ) {
606 (Ok(username), Ok(password)) => Some(BasicAuth::new(username, password)),
607 _ => Some(BasicAuth::new(
608 "spark-sdk".to_string(),
609 "mCMk1JqlBNtetUNy".to_string(),
610 )),
611 },
612 ChainApiType::MempoolSpace,
613 )),
614 }
615 };
616
617 #[cfg(feature = "postgres")]
619 let has_postgres = postgres_pool.is_some();
620 #[cfg(not(feature = "postgres"))]
621 let has_postgres = false;
622
623 #[cfg(feature = "mysql")]
624 let has_mysql = mysql_pool.is_some();
625 #[cfg(not(feature = "mysql"))]
626 let has_mysql = false;
627
628 let storage_count = [
629 self.storage.is_some(),
630 self.storage_dir.is_some(),
631 has_postgres,
632 has_mysql,
633 ]
634 .into_iter()
635 .filter(|&v| v)
636 .count();
637 match storage_count {
638 0 => return Err(SdkError::Generic("No storage configured".to_string())),
639 2.. => {
640 return Err(SdkError::Generic(
641 "Multiple storage configurations provided".to_string(),
642 ));
643 }
644 _ => {}
645 }
646
647 #[cfg(feature = "postgres")]
651 let postgres_backend = if let Some(ref pool) = postgres_pool {
652 let identity = spark_signer
653 .get_identity_public_key()
654 .await
655 .map_err(|e| SdkError::Generic(e.to_string()))?
656 .serialize();
657 Some((pool.inner.clone(), identity, pool.run_migration))
658 } else {
659 None
660 };
661
662 #[cfg(feature = "mysql")]
664 let mysql_backend = if let Some(ref pool) = mysql_pool {
665 let identity = spark_signer
666 .get_identity_public_key()
667 .await
668 .map_err(|e| SdkError::Generic(e.to_string()))?
669 .serialize();
670 Some((
671 pool.inner.clone(),
672 identity,
673 pool.run_migration,
674 pool.foreign_key_mode,
675 ))
676 } else {
677 None
678 };
679
680 let storage: Arc<dyn Storage> = if let Some(storage) = self.storage {
682 storage
683 } else if let Some(storage_dir) = self.storage_dir {
684 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
685 {
686 let identity_pub_key = spark_signer
687 .get_identity_public_key()
688 .await
689 .map_err(|e| SdkError::Generic(e.to_string()))?;
690 default_storage(&storage_dir, self.config.network, &identity_pub_key)?
691 }
692 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
693 {
694 let _ = storage_dir;
695 return Err(SdkError::Generic(
696 "with_default_storage is not supported on WASM".to_string(),
697 ));
698 }
699 } else {
700 #[allow(unused_mut)]
701 let mut s: Option<Arc<dyn Storage>> = None;
702
703 #[cfg(all(
704 feature = "postgres",
705 not(all(target_family = "wasm", target_os = "unknown"))
706 ))]
707 if s.is_none()
708 && let Some((ref pool, ref identity, run_migration)) = postgres_backend
709 {
710 s = Some(Arc::new(
711 crate::persist::postgres::PostgresStorage::new_with_pool(
712 pool.clone(),
713 identity,
714 run_migration,
715 )
716 .await
717 .map_err(|e| SdkError::Generic(e.to_string()))?,
718 ));
719 }
720
721 #[cfg(all(
722 feature = "mysql",
723 not(all(target_family = "wasm", target_os = "unknown"))
724 ))]
725 if s.is_none()
726 && let Some((ref pool, ref identity, run_migration, _)) = mysql_backend
727 {
728 s = Some(Arc::new(
729 crate::persist::mysql::MysqlStorage::new_with_pool(
730 pool.clone(),
731 identity,
732 run_migration,
733 )
734 .await
735 .map_err(|e| SdkError::Generic(e.to_string()))?,
736 ));
737 }
738
739 s.ok_or_else(|| SdkError::Generic("No storage configured".to_string()))?
740 };
741
742 let user_agent = crate::default_user_agent();
743 info!("Building sdk with user agent: {}", user_agent);
744
745 let breez_server = Arc::clone(&context.breez_server);
746
747 let fiat_service: Arc<dyn breez_sdk_common::fiat::FiatService> = match self.fiat_service {
748 Some(service) => Arc::new(FiatServiceWrapper::new(service)),
749 None => breez_server.clone(),
750 };
751
752 let lnurl_client: Arc<dyn platform_utils::HttpClient> = match self.lnurl_client {
753 Some(client) => client,
754 None => context.http_client.clone(),
755 };
756 let mut spark_wallet_config = if let Some(env_config) = &self.config.spark_config {
757 Self::build_spark_wallet_config(self.config.network.into(), env_config)?
758 } else {
759 spark_wallet::SparkWalletConfig::default_config(self.config.network.into())
760 };
761 spark_wallet_config.operator_pool = spark_wallet_config
762 .operator_pool
763 .with_user_agent(Some(user_agent.clone()));
764 spark_wallet_config.service_provider_config.user_agent = Some(user_agent.clone());
765 let background_services_enabled = runtime.starts_background_services();
766 spark_wallet_config.leaf_auto_optimize_enabled =
767 background_services_enabled && self.config.leaf_optimization_config.auto_enabled;
768 spark_wallet_config.leaf_optimization_options.multiplicity =
769 self.config.leaf_optimization_config.multiplicity;
770
771 let token_opt = &self.config.token_optimization_config;
772 let token_options = &mut spark_wallet_config.token_outputs_optimization_options;
773 token_options.target_output_count = token_opt.target_output_count;
774 token_options.min_outputs_threshold = token_opt.min_outputs_threshold;
775 if !token_opt.auto_enabled || !background_services_enabled {
777 token_options.auto_optimize_interval = None;
778 }
779 spark_wallet_config.max_concurrent_claims = self.config.max_concurrent_claims;
780
781 let shutdown_sender = watch::channel::<()>(()).0;
782
783 #[allow(unused_mut)]
785 let mut tree_store: Option<Arc<dyn TreeStore>> = self.tree_store;
786
787 #[cfg(feature = "postgres")]
788 if tree_store.is_none()
789 && let Some((ref pool, ref identity, run_migration)) = postgres_backend
790 {
791 tree_store = Some(
792 crate::persist::postgres::create_postgres_tree_store(
793 pool.clone(),
794 identity,
795 run_migration,
796 )
797 .await?,
798 );
799 }
800
801 #[cfg(feature = "mysql")]
802 if tree_store.is_none()
803 && let Some((ref pool, ref identity, run_migration, foreign_key_mode)) = mysql_backend
804 {
805 tree_store = Some(
806 crate::persist::mysql::create_mysql_tree_store(
807 pool.clone(),
808 identity,
809 run_migration,
810 foreign_key_mode,
811 )
812 .await?,
813 );
814 }
815
816 #[allow(unused_mut)]
818 let mut token_output_store: Option<Arc<dyn TokenOutputStore>> = self.token_output_store;
819
820 #[cfg(feature = "postgres")]
821 if token_output_store.is_none()
822 && let Some((ref pool, ref identity, run_migration)) = postgres_backend
823 {
824 token_output_store = Some(
825 crate::persist::postgres::create_postgres_token_store(
826 pool.clone(),
827 identity,
828 run_migration,
829 )
830 .await?,
831 );
832 }
833
834 #[cfg(feature = "mysql")]
835 if token_output_store.is_none()
836 && let Some((ref pool, ref identity, run_migration, foreign_key_mode)) = mysql_backend
837 {
838 token_output_store = Some(
839 crate::persist::mysql::create_mysql_token_store(
840 pool.clone(),
841 identity,
842 run_migration,
843 foreign_key_mode,
844 )
845 .await?,
846 );
847 }
848
849 #[allow(unused_mut)]
850 let mut inner_session_manager: Option<Arc<dyn spark_wallet::SessionManager>> = self
851 .session_manager
852 .map(|sm| Arc::new(SessionManagerAdapter(sm)) as Arc<dyn spark_wallet::SessionManager>);
853
854 #[cfg(feature = "postgres")]
855 if inner_session_manager.is_none()
856 && let Some((ref pool, ref identity, run_migration)) = postgres_backend
857 {
858 inner_session_manager = Some(
859 crate::persist::postgres::create_postgres_session_manager(
860 pool.clone(),
861 identity,
862 run_migration,
863 )
864 .await?,
865 );
866 }
867
868 #[cfg(feature = "mysql")]
869 if inner_session_manager.is_none()
870 && let Some((ref pool, ref identity, run_migration, _)) = mysql_backend
871 {
872 inner_session_manager = Some(
873 crate::persist::mysql::create_mysql_session_manager(
874 pool.clone(),
875 identity,
876 run_migration,
877 )
878 .await?,
879 );
880 }
881
882 let inner_session_manager =
883 inner_session_manager.unwrap_or_else(|| Arc::new(InMemorySessionManager::default()));
884 let inner_session_manager: Arc<dyn spark_wallet::SessionManager> = Arc::new(
885 crate::session_manager::EncryptingSessionManager::new(
886 inner_session_manager,
887 signer.clone(),
888 self.config.network,
889 )
890 .map_err(|e| {
891 SdkError::Generic(format!("failed to set up session token encryption: {e}"))
892 })?,
893 );
894 let inner_session_manager: Arc<dyn spark_wallet::SessionManager> = Arc::new(
895 crate::session_manager::CachingSessionManager::new(inner_session_manager),
896 );
897 let mut wallet_builder =
898 spark_wallet::WalletBuilder::new(spark_wallet_config, spark_signer)
899 .with_cancellation_token(shutdown_sender.subscribe())
900 .with_session_manager(inner_session_manager)
901 .with_background_processing(background_services_enabled);
902 if let Some(provider) = &context.jwt_header_provider {
903 wallet_builder = wallet_builder.with_so_extra_header_provider(
904 Arc::clone(provider) as Arc<dyn spark_wallet::HeaderProvider>
905 );
906 }
907 if let Some(observer) = self.payment_observer {
908 let observer: Arc<dyn spark_wallet::TransferObserver> =
909 Arc::new(SparkTransferObserver::new(observer));
910 wallet_builder = wallet_builder.with_transfer_observer(observer);
911 }
912 if let Some(tree_store) = tree_store {
913 wallet_builder = wallet_builder.with_tree_store(tree_store);
914 }
915 if let Some(token_output_store) = token_output_store {
916 wallet_builder = wallet_builder.with_token_output_store(token_output_store);
917 }
918 wallet_builder = wallet_builder.with_ssp_http_client(context.http_client.clone());
919 wallet_builder = wallet_builder.with_connection_manager(context.connection_manager.clone());
920 let spark_wallet = Arc::new(wallet_builder.build().await?);
921
922 let lnurl_server_client: Option<Arc<dyn LnurlServerClient>> = match self.lnurl_server_client
923 {
924 Some(client) => Some(client),
925 None => match &self.config.lnurl_domain {
926 Some(domain) => Some(Arc::new(DefaultLnurlServerClient::new(
927 context.http_client.clone(),
928 domain.clone(),
929 self.config.api_key.clone(),
930 Arc::clone(&spark_wallet),
931 ))),
932 None => None,
933 },
934 };
935
936 let real_time_sync_active =
937 background_services_enabled && self.config.real_time_sync_server_url.is_some();
938 let event_emitter = Arc::new(EventEmitter::new(real_time_sync_active));
939
940 let storage = match &self.config.real_time_sync_server_url {
941 Some(server_url) if background_services_enabled => {
942 init_and_start_real_time_sync(RealTimeSyncParams {
943 server_url: server_url.clone(),
944 api_key: self.config.api_key.clone(),
945 user_agent,
946 signer: rtsync_signer,
947 storage: Arc::clone(&storage),
948 shutdown_receiver: shutdown_sender.subscribe(),
949 event_emitter: Arc::clone(&event_emitter),
950 lnurl_server_client: lnurl_server_client.clone(),
951 })
952 .await?
953 }
954 _ => storage,
955 };
956
957 let buy_bitcoin_provider = Arc::new(MoonpayProvider::new(breez_server.clone()));
959
960 let flashnet_config = FlashnetConfig::default_config(
962 self.config.network.into(),
963 DEFAULT_INTEGRATOR_PUBKEY
964 .parse()
965 .ok()
966 .map(|pubkey| IntegratorConfig {
967 pubkey,
968 fee_bps: DEFAULT_INTEGRATOR_FEE_BPS,
969 }),
970 );
971 let flashnet_converter = Arc::new(FlashnetTokenConverter::new(
972 flashnet_config,
973 Arc::clone(&storage),
974 Arc::clone(&spark_wallet),
975 self.config.network,
976 context.http_client.clone(),
977 ));
978 let token_converter: Arc<dyn TokenConverter> = flashnet_converter;
979
980 let sync_coordinator = SyncCoordinator::new();
982 let stable_balance = if let Some(config) = &self.config.stable_balance_config {
986 let stable_balance = Arc::new(
987 StableBalance::new(
988 config.clone(),
989 Arc::clone(&token_converter),
990 Arc::clone(&spark_wallet),
991 Arc::clone(&storage),
992 Arc::clone(&event_emitter),
993 )
994 .await,
995 );
996 Some(stable_balance)
997 } else {
998 None
999 };
1000
1001 event_emitter
1004 .add_middleware(Box::new(TokenConversionMiddleware))
1005 .await;
1006
1007 let sdk = BreezSdk::init_and_start(BreezSdkParams {
1009 config: self.config,
1010 storage,
1011 chain_service,
1012 fiat_service,
1013 lnurl_client,
1014 lnurl_server_client,
1015 lnurl_auth_signer,
1016 shutdown_sender,
1017 runtime,
1018 spark_wallet,
1019 event_emitter,
1020 buy_bitcoin_provider,
1021 token_converter,
1022 stable_balance,
1023 sync_coordinator,
1024 })
1025 .await?;
1026 debug!("Initialized and started breez sdk.");
1027
1028 Ok(sdk)
1029 }
1030}
1031
1032#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1033fn default_storage(
1034 data_dir: &str,
1035 network: Network,
1036 identity_pub_key: &spark_wallet::PublicKey,
1037) -> Result<Arc<dyn Storage>, SdkError> {
1038 let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
1039 let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
1040 Ok(storage)
1041}
1042
1043#[cfg(test)]
1044#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1045mod tests {
1046 use super::SdkBuilder;
1047 use crate::{Network, default_config};
1048
1049 #[test]
1050 fn default_config_spark_config_builds_valid_wallet_config() {
1051 for network in [Network::Mainnet, Network::Regtest] {
1052 let config = default_config(network);
1053 let spark_config = config
1054 .spark_config
1055 .as_ref()
1056 .expect("default_config must populate spark_config");
1057 SdkBuilder::build_spark_wallet_config(network.into(), spark_config).unwrap_or_else(
1058 |e| {
1059 panic!(
1060 "default_config({network:?}).spark_config failed to build SparkWalletConfig: {e}"
1061 )
1062 },
1063 );
1064 }
1065 }
1066
1067 #[tokio::test]
1068 async fn server_mode_rejects_stable_balance_config() {
1069 use crate::{SdkError, StableBalanceConfig, StableBalanceToken, default_server_config};
1070
1071 let mut config = default_server_config(Network::Regtest);
1072 config.stable_balance_config = Some(StableBalanceConfig {
1073 tokens: vec![StableBalanceToken {
1074 label: "USDB".to_string(),
1075 token_identifier: "btkn1test".to_string(),
1076 }],
1077 default_active_label: None,
1078 threshold_sats: None,
1079 max_slippage_bps: None,
1080 });
1081
1082 let seed = test_seed();
1083 let result = SdkBuilder::new(config, seed).build().await;
1084 match result {
1085 Err(SdkError::InvalidInput(message)) => {
1086 assert!(message.contains("stable_balance_config"));
1087 }
1088 Err(err) => panic!("expected InvalidInput error, got {err:?}"),
1089 Ok(_) => panic!("expected server mode with Stable Balance config to fail"),
1090 }
1091 }
1092
1093 #[tokio::test]
1094 async fn server_mode_rejects_real_time_sync_server_url() {
1095 use crate::{SdkError, default_server_config};
1096
1097 let mut config = default_server_config(Network::Regtest);
1098 config.real_time_sync_server_url = Some("https://example.com".to_string());
1099
1100 let seed = test_seed();
1101 let result = SdkBuilder::new(config, seed).build().await;
1102 match result {
1103 Err(SdkError::InvalidInput(message)) => {
1104 assert!(message.contains("real_time_sync_server_url"));
1105 }
1106 Err(err) => panic!("expected InvalidInput error, got {err:?}"),
1107 Ok(_) => panic!("expected server mode with real_time_sync_server_url to fail"),
1108 }
1109 }
1110
1111 #[tokio::test]
1112 async fn server_mode_rejects_leaf_optimization_auto_enabled() {
1113 use crate::{SdkError, default_server_config};
1114
1115 let mut config = default_server_config(Network::Regtest);
1116 config.leaf_optimization_config.auto_enabled = true;
1117
1118 let seed = test_seed();
1119 let result = SdkBuilder::new(config, seed).build().await;
1120 match result {
1121 Err(SdkError::InvalidInput(message)) => {
1122 assert!(message.contains("leaf_optimization_config.auto_enabled"));
1123 }
1124 Err(err) => panic!("expected InvalidInput error, got {err:?}"),
1125 Ok(_) => panic!("expected server mode with optimization auto_enabled to fail"),
1126 }
1127 }
1128
1129 #[tokio::test]
1130 async fn server_mode_rejects_token_optimization_auto_enabled() {
1131 use crate::{SdkError, default_server_config};
1132
1133 let mut config = default_server_config(Network::Regtest);
1134 config.token_optimization_config.auto_enabled = true;
1135
1136 let seed = test_seed();
1137 let result = SdkBuilder::new(config, seed).build().await;
1138 match result {
1139 Err(SdkError::InvalidInput(message)) => {
1140 assert!(message.contains("token_optimization_config.auto_enabled"));
1141 }
1142 Err(err) => panic!("expected InvalidInput error, got {err:?}"),
1143 Ok(_) => panic!("expected server mode with optimization auto_enabled to fail"),
1144 }
1145 }
1146
1147 #[tokio::test]
1151 async fn build_errors_on_network_mismatch() {
1152 use crate::{SdkContextConfig, new_shared_sdk_context};
1153 let mut config = default_config(Network::Mainnet);
1154 config.api_key = Some("partner-key".to_string());
1155 let ctx = new_shared_sdk_context(SdkContextConfig {
1156 api_key: Some("partner-key".to_string()),
1157 ..SdkContextConfig::new(Network::Regtest)
1158 })
1159 .await
1160 .expect("regtest context");
1161 let err = SdkBuilder::new(config, test_seed())
1162 .with_shared_context(ctx)
1163 .with_default_storage("/tmp/breez-sdk-test-network-mismatch".to_string())
1164 .build()
1165 .await
1166 .err()
1167 .expect("expected network-mismatch error");
1168 assert!(
1169 err.to_string().contains("network/api_key do not match"),
1170 "unexpected error: {err}"
1171 );
1172 }
1173
1174 #[tokio::test]
1178 #[allow(clippy::manual_assert)]
1179 async fn build_errors_on_api_key_mismatch() {
1180 use crate::{SdkContextConfig, new_shared_sdk_context};
1181 let mut config = default_config(Network::Mainnet);
1182 config.api_key = Some("intended-key".to_string());
1183 let ctx = new_shared_sdk_context(SdkContextConfig {
1184 api_key: Some("wrong-key".to_string()),
1185 ..SdkContextConfig::new(Network::Mainnet)
1186 })
1187 .await
1188 .expect("mainnet context");
1189 let err = SdkBuilder::new(config, test_seed())
1190 .with_shared_context(ctx)
1191 .with_default_storage("/tmp/breez-sdk-test-key-mismatch".to_string())
1192 .build()
1193 .await
1194 .err()
1195 .expect("expected api_key-mismatch error");
1196 assert!(
1197 err.to_string().contains("network/api_key do not match"),
1198 "unexpected error: {err}"
1199 );
1200 }
1201
1202 fn test_seed() -> crate::Seed {
1203 crate::Seed::Mnemonic {
1204 mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon \
1205 abandon abandon about"
1206 .to_string(),
1207 passphrase: None,
1208 }
1209 }
1210}