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