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#[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#[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 #[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 #[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 #[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 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 pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
166 self.storage = Some(storage);
167 self
168 }
169
170 #[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 #[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 #[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 #[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 #[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 #[allow(clippy::too_many_lines)]
259 pub async fn build(self) -> Result<BreezSdk, SdkError> {
260 self.config.validate()?;
262
263 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 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 #[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 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 let buy_bitcoin_provider: Arc<dyn BuyBitcoinProviderApi> =
482 Arc::new(MoonpayProvider::new(breez_server.clone()));
483
484 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}