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 rest::ReqwestRestClient as CommonRequestRestClient,
10};
11
12use flashnet::{CacheStore, FlashnetClient, FlashnetConfig};
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, RestClient,
20 RestClientWrapper, Seed,
21 chain::{
22 BitcoinChainService,
23 rest_client::{BasicAuth, ChainApiType, RestClientChainService},
24 },
25 error::SdkError,
26 lnurl::{LnurlServerClient, ReqwestLnurlServerClient},
27 models::Config,
28 nostr::NostrClient,
29 payment_observer::{PaymentObserver, SparkTransferObserver},
30 persist::Storage,
31 realtime_sync::{RealTimeSyncParams, init_and_start_real_time_sync},
32 sdk::{BreezSdk, BreezSdkParams},
33 signer::{
34 breez::BreezSignerImpl, nostr::NostrSigner, rtsync::RTSyncSigner, spark::SparkSigner,
35 },
36 sync_storage::SyncStorage,
37};
38
39#[derive(Clone)]
41enum SignerSource {
42 Seed {
43 seed: Seed,
44 key_set_type: KeySetType,
45 use_address_index: bool,
46 account_number: Option<u32>,
47 },
48 External(Arc<dyn crate::signer::ExternalSigner>),
49}
50
51#[derive(Clone)]
53pub struct SdkBuilder {
54 config: Config,
55 signer_source: SignerSource,
56
57 storage_dir: Option<String>,
58 storage: Option<Arc<dyn Storage>>,
59 chain_service: Option<Arc<dyn BitcoinChainService>>,
60 fiat_service: Option<Arc<dyn FiatService>>,
61 lnurl_client: Option<Arc<dyn RestClient>>,
62 lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
63 payment_observer: Option<Arc<dyn PaymentObserver>>,
64 sync_storage: Option<Arc<dyn SyncStorage>>,
65}
66
67impl SdkBuilder {
68 #[allow(clippy::needless_pass_by_value)]
76 pub fn new(config: Config, seed: Seed) -> Self {
77 SdkBuilder {
78 config,
79 signer_source: SignerSource::Seed {
80 seed,
81 key_set_type: KeySetType::Default,
82 use_address_index: false,
83 account_number: None,
84 },
85 storage_dir: None,
86 storage: None,
87 chain_service: None,
88 fiat_service: None,
89 lnurl_client: None,
90 lnurl_server_client: None,
91 payment_observer: None,
92 sync_storage: None,
93 }
94 }
95
96 #[allow(clippy::needless_pass_by_value)]
102 pub fn new_with_signer(config: Config, signer: Arc<dyn crate::signer::ExternalSigner>) -> Self {
103 SdkBuilder {
104 config,
105 signer_source: SignerSource::External(signer),
106 storage_dir: None,
107 storage: None,
108 chain_service: None,
109 fiat_service: None,
110 lnurl_client: None,
111 lnurl_server_client: None,
112 payment_observer: None,
113 sync_storage: None,
114 }
115 }
116
117 #[must_use]
125 pub fn with_key_set(mut self, config: crate::models::KeySetConfig) -> Self {
126 if let SignerSource::Seed {
127 key_set_type: ref mut kst,
128 use_address_index: ref mut uai,
129 account_number: ref mut an,
130 ..
131 } = self.signer_source
132 {
133 *kst = config.key_set_type;
134 *uai = config.use_address_index;
135 *an = config.account_number;
136 }
137 self
138 }
139
140 #[must_use]
141 pub fn with_default_storage(mut self, storage_dir: String) -> Self {
147 self.storage_dir = Some(storage_dir);
148 self
149 }
150
151 #[must_use]
152 pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
156 self.storage = Some(storage);
157 self
158 }
159
160 #[must_use]
161 pub fn with_real_time_sync_storage(mut self, storage: Arc<dyn SyncStorage>) -> Self {
165 self.sync_storage = Some(storage);
166 self
167 }
168
169 #[must_use]
173 pub fn with_chain_service(mut self, chain_service: Arc<dyn BitcoinChainService>) -> Self {
174 self.chain_service = Some(chain_service);
175 self
176 }
177
178 #[must_use]
184 pub fn with_rest_chain_service(
185 mut self,
186 url: String,
187 api_type: ChainApiType,
188 credentials: Option<Credentials>,
189 ) -> Self {
190 self.chain_service = Some(Arc::new(RestClientChainService::new(
191 url,
192 self.config.network,
193 5,
194 Box::new(CommonRequestRestClient::new().unwrap()),
195 credentials.map(|c| BasicAuth::new(c.username, c.password)),
196 api_type,
197 )));
198 self
199 }
200
201 #[must_use]
205 pub fn with_fiat_service(mut self, fiat_service: Arc<dyn FiatService>) -> Self {
206 self.fiat_service = Some(fiat_service);
207 self
208 }
209
210 #[must_use]
211 pub fn with_lnurl_client(mut self, lnurl_client: Arc<dyn RestClient>) -> Self {
212 self.lnurl_client = Some(lnurl_client);
213 self
214 }
215
216 #[must_use]
217 #[allow(unused)]
218 pub fn with_lnurl_server_client(
219 mut self,
220 lnurl_serverclient: Arc<dyn LnurlServerClient>,
221 ) -> Self {
222 self.lnurl_server_client = Some(lnurl_serverclient);
223 self
224 }
225
226 #[must_use]
231 #[allow(unused)]
232 pub fn with_payment_observer(mut self, payment_observer: Arc<dyn PaymentObserver>) -> Self {
233 self.payment_observer = Some(payment_observer);
234 self
235 }
236
237 #[allow(clippy::too_many_lines)]
239 pub async fn build(self) -> Result<BreezSdk, SdkError> {
240 let (signer, account_number) = match self.signer_source {
242 SignerSource::Seed {
243 seed,
244 key_set_type,
245 use_address_index,
246 account_number,
247 } => {
248 let breez_signer = Arc::new(
249 BreezSignerImpl::new(
250 &self.config,
251 &seed,
252 key_set_type.into(),
253 use_address_index,
254 account_number,
255 )
256 .map_err(|e| SdkError::Generic(e.to_string()))?,
257 );
258 (
259 breez_signer as Arc<dyn crate::signer::BreezSigner>,
260 account_number,
261 )
262 }
263 SignerSource::External(external_signer) => {
264 use crate::signer::ExternalSignerAdapter;
265 let adapter = Arc::new(ExternalSignerAdapter::new(external_signer));
266 (adapter as Arc<dyn crate::signer::BreezSigner>, None)
267 }
268 };
269
270 let spark_signer = Arc::new(SparkSigner::new(signer.clone()));
272 let rtsync_signer = Arc::new(
273 RTSyncSigner::new(signer.clone(), self.config.network)
274 .map_err(|e| SdkError::Generic(e.to_string()))?,
275 );
276 let nostr_signer = Arc::new(
277 NostrSigner::new(signer.clone(), self.config.network, account_number)
278 .await
279 .map_err(|e| SdkError::Generic(format!("{e:?}")))?,
280 );
281
282 let chain_service = if let Some(service) = self.chain_service {
283 service
284 } else {
285 let inner_client =
286 CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?;
287 match self.config.network {
288 Network::Mainnet => Arc::new(RestClientChainService::new(
289 "https://blockstream.info/api".to_string(),
290 self.config.network,
291 5,
292 Box::new(inner_client),
293 None,
294 ChainApiType::Esplora,
295 )),
296 Network::Regtest => Arc::new(RestClientChainService::new(
297 "https://regtest-mempool.us-west-2.sparkinfra.net/api".to_string(),
298 self.config.network,
299 5,
300 Box::new(inner_client),
301 match (
302 std::env::var("CHAIN_SERVICE_USERNAME"),
303 std::env::var("CHAIN_SERVICE_PASSWORD"),
304 ) {
305 (Ok(username), Ok(password)) => Some(BasicAuth::new(username, password)),
306 _ => Some(BasicAuth::new(
307 "spark-sdk".to_string(),
308 "mCMk1JqlBNtetUNy".to_string(),
309 )),
310 },
311 ChainApiType::MempoolSpace,
312 )),
313 }
314 };
315
316 let (storage, sync_storage) = match (self.storage, self.storage_dir) {
317 (Some(storage), _) => (storage, self.sync_storage),
319 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
321 (None, Some(storage_dir)) => {
322 let identity_pub_key = spark_signer
323 .get_identity_public_key()
324 .await
325 .map_err(|e| SdkError::Generic(e.to_string()))?;
326 let storage =
327 default_storage(&storage_dir, self.config.network, &identity_pub_key)?;
328 let sync_storage = match (self.sync_storage, &self.config.real_time_sync_server_url)
329 {
330 (Some(sync_storage), _) => Some(sync_storage),
332 (None, Some(_)) => Some(default_sync_storage(
335 &storage_dir,
336 self.config.network,
337 &identity_pub_key,
338 )?),
339 _ => None,
340 };
341 (storage, sync_storage)
342 }
343 _ => {
344 return Err(SdkError::Generic(
345 "Either storage or storage_dir must be set before building the SDK".to_string(),
346 ));
347 }
348 };
349
350 let fiat_service: Arc<dyn breez_sdk_common::fiat::FiatService> = match self.fiat_service {
351 Some(service) => Arc::new(FiatServiceWrapper::new(service)),
352 None => Arc::new(
353 BreezServer::new(PRODUCTION_BREEZSERVER_URL, None)
354 .map_err(|e| SdkError::Generic(e.to_string()))?,
355 ),
356 };
357
358 let lnurl_client: Arc<dyn breez_sdk_common::rest::RestClient> = match self.lnurl_client {
359 Some(client) => Arc::new(RestClientWrapper::new(client)),
360 None => Arc::new(
361 CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?,
362 ),
363 };
364 let user_agent = format!(
365 "{}/{}",
366 crate::built_info::PKG_NAME,
367 crate::built_info::GIT_VERSION.unwrap_or(crate::built_info::PKG_VERSION),
368 );
369 info!("Building SparkWallet with user agent: {}", user_agent);
370 let mut spark_wallet_config =
371 spark_wallet::SparkWalletConfig::default_config(self.config.network.into());
372 spark_wallet_config.operator_pool = spark_wallet_config
373 .operator_pool
374 .with_user_agent(Some(user_agent.clone()));
375 spark_wallet_config.service_provider_config.user_agent = Some(user_agent);
376 spark_wallet_config.leaf_auto_optimize_enabled =
377 self.config.optimization_config.auto_enabled;
378 spark_wallet_config.leaf_optimization_options.multiplicity =
379 self.config.optimization_config.multiplicity;
380
381 let mut wallet_builder =
382 spark_wallet::WalletBuilder::new(spark_wallet_config, spark_signer);
383 if let Some(observer) = self.payment_observer {
384 let observer: Arc<dyn spark_wallet::TransferObserver> =
385 Arc::new(SparkTransferObserver::new(observer));
386 wallet_builder = wallet_builder.with_transfer_observer(observer);
387 }
388 let spark_wallet = Arc::new(wallet_builder.build().await?);
389
390 let lnurl_server_client: Option<Arc<dyn LnurlServerClient>> = match self.lnurl_server_client
391 {
392 Some(client) => Some(client),
393 None => match &self.config.lnurl_domain {
394 Some(domain) => {
395 Some(Arc::new(ReqwestLnurlServerClient::new(
397 domain.clone(),
398 self.config.api_key.clone(),
399 Arc::clone(&spark_wallet),
400 )?))
401 }
402 None => None,
403 },
404 };
405 let shutdown_sender = watch::channel::<()>(()).0;
406
407 let event_emitter = Arc::new(EventEmitter::new(
408 self.config.real_time_sync_server_url.is_some(),
409 ));
410 let storage = if let Some(server_url) = &self.config.real_time_sync_server_url {
411 let Some(sync_storage) = sync_storage else {
412 return Err(SdkError::Generic(
413 "Real-time sync is enabled, but no sync storage is supplied".to_string(),
414 ));
415 };
416
417 init_and_start_real_time_sync(RealTimeSyncParams {
418 server_url: server_url.clone(),
419 api_key: self.config.api_key.clone(),
420 signer: rtsync_signer,
421 storage: Arc::clone(&storage),
422 sync_storage,
423 shutdown_receiver: shutdown_sender.subscribe(),
424 event_emitter: Arc::clone(&event_emitter),
425 })
426 .await?
427 } else {
428 storage
429 };
430 let flashnet_client = Arc::new(FlashnetClient::new(
431 FlashnetConfig::default_config(self.config.network.into()),
432 spark_wallet.clone(),
433 Arc::new(CacheStore::default()),
434 ));
435
436 let nostr_client = Arc::new(NostrClient::new(nostr_signer));
437
438 let sdk = BreezSdk::init_and_start(BreezSdkParams {
440 config: self.config,
441 storage,
442 chain_service,
443 fiat_service,
444 lnurl_client,
445 lnurl_server_client,
446 shutdown_sender,
447 spark_wallet,
448 event_emitter,
449 nostr_client,
450 flashnet_client,
451 })?;
452 debug!("Initialized and started breez sdk.");
453
454 Ok(sdk)
455 }
456}
457
458#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
459fn default_storage(
460 data_dir: &str,
461 network: Network,
462 identity_pub_key: &spark_wallet::PublicKey,
463) -> Result<Arc<dyn Storage>, SdkError> {
464 let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
465 let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
466 Ok(storage)
467}
468
469#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
470fn default_sync_storage(
471 data_dir: &str,
472 network: Network,
473 identity_pub_key: &spark_wallet::PublicKey,
474) -> Result<Arc<dyn SyncStorage>, SdkError> {
475 let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
476 let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
477 Ok(storage)
478}