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 bitcoin::bip32::Xpriv;
8use breez_sdk_common::{
9 breez_server::{BreezServer, PRODUCTION_BREEZSERVER_URL},
10 rest::ReqwestRestClient as CommonRequestRestClient,
11};
12use spark_wallet::{DefaultSigner, KeySet, Signer};
13use tokio::sync::watch;
14use tracing::{debug, info};
15
16use crate::{
17 Credentials, EventEmitter, FiatService, FiatServiceWrapper, KeySetType, Network, RestClient,
18 RestClientWrapper, Seed,
19 chain::{
20 BitcoinChainService,
21 rest_client::{BasicAuth, ChainApiType, RestClientChainService},
22 },
23 error::SdkError,
24 lnurl::{LnurlServerClient, ReqwestLnurlServerClient},
25 models::Config,
26 nostr::NostrClient,
27 payment_observer::{PaymentObserver, SparkTransferObserver},
28 persist::Storage,
29 realtime_sync::{RealTimeSyncParams, init_and_start_real_time_sync},
30 sdk::{BreezSdk, BreezSdkParams},
31 sync_storage::SyncStorage,
32};
33
34#[derive(Clone)]
36pub struct SdkBuilder {
37 config: Config,
38 seed: Seed,
39 storage_dir: Option<String>,
40 storage: Option<Arc<dyn Storage>>,
41 chain_service: Option<Arc<dyn BitcoinChainService>>,
42 fiat_service: Option<Arc<dyn FiatService>>,
43 lnurl_client: Option<Arc<dyn RestClient>>,
44 lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
45 payment_observer: Option<Arc<dyn PaymentObserver>>,
46 key_set_type: KeySetType,
47 use_address_index: bool,
48 account_number: Option<u32>,
49 sync_storage: Option<Arc<dyn SyncStorage>>,
50}
51
52impl SdkBuilder {
53 pub fn new(config: Config, seed: Seed) -> Self {
58 SdkBuilder {
59 config,
60 seed,
61 storage_dir: None,
62 storage: None,
63 chain_service: None,
64 fiat_service: None,
65 lnurl_client: None,
66 lnurl_server_client: None,
67 payment_observer: None,
68 key_set_type: KeySetType::Default,
69 use_address_index: false,
70 account_number: None,
71 sync_storage: None,
72 }
73 }
74
75 #[must_use]
76 pub fn with_default_storage(mut self, storage_dir: String) -> Self {
82 self.storage_dir = Some(storage_dir);
83 self
84 }
85
86 #[must_use]
87 pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
91 self.storage = Some(storage);
92 self
93 }
94
95 #[must_use]
96 pub fn with_real_time_sync_storage(mut self, storage: Arc<dyn SyncStorage>) -> Self {
100 self.sync_storage = Some(storage);
101 self
102 }
103
104 #[must_use]
109 pub fn with_key_set(
110 mut self,
111 key_set_type: KeySetType,
112 use_address_index: bool,
113 account_number: Option<u32>,
114 ) -> Self {
115 self.key_set_type = key_set_type;
116 self.use_address_index = use_address_index;
117 self.account_number = account_number;
118 self
119 }
120
121 #[must_use]
125 pub fn with_chain_service(mut self, chain_service: Arc<dyn BitcoinChainService>) -> Self {
126 self.chain_service = Some(chain_service);
127 self
128 }
129
130 #[must_use]
136 pub fn with_rest_chain_service(
137 mut self,
138 url: String,
139 api_type: ChainApiType,
140 credentials: Option<Credentials>,
141 ) -> Self {
142 self.chain_service = Some(Arc::new(RestClientChainService::new(
143 url,
144 self.config.network,
145 5,
146 Box::new(CommonRequestRestClient::new().unwrap()),
147 credentials.map(|c| BasicAuth::new(c.username, c.password)),
148 api_type,
149 )));
150 self
151 }
152
153 #[must_use]
157 pub fn with_fiat_service(mut self, fiat_service: Arc<dyn FiatService>) -> Self {
158 self.fiat_service = Some(fiat_service);
159 self
160 }
161
162 #[must_use]
163 pub fn with_lnurl_client(mut self, lnurl_client: Arc<dyn RestClient>) -> Self {
164 self.lnurl_client = Some(lnurl_client);
165 self
166 }
167
168 #[must_use]
169 #[allow(unused)]
170 pub fn with_lnurl_server_client(
171 mut self,
172 lnurl_serverclient: Arc<dyn LnurlServerClient>,
173 ) -> Self {
174 self.lnurl_server_client = Some(lnurl_serverclient);
175 self
176 }
177
178 #[must_use]
183 #[allow(unused)]
184 pub fn with_payment_observer(mut self, payment_observer: Arc<dyn PaymentObserver>) -> Self {
185 self.payment_observer = Some(payment_observer);
186 self
187 }
188
189 #[allow(clippy::too_many_lines)]
191 pub async fn build(self) -> Result<BreezSdk, SdkError> {
192 let seed_bytes = self.seed.to_bytes()?;
194 let key_set = KeySet::new(
195 &seed_bytes,
196 self.config.network.into(),
197 self.key_set_type.into(),
198 self.use_address_index,
199 self.account_number,
200 )
201 .map_err(|e| SdkError::Generic(e.to_string()))?;
202 let signer: Arc<dyn Signer> = Arc::new(DefaultSigner::from_key_set(key_set.clone()));
203
204 let chain_service = if let Some(service) = self.chain_service {
205 service
206 } else {
207 let inner_client =
208 CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?;
209 match self.config.network {
210 Network::Mainnet => Arc::new(RestClientChainService::new(
211 "https://blockstream.info/api".to_string(),
212 self.config.network,
213 5,
214 Box::new(inner_client),
215 None,
216 ChainApiType::Esplora,
217 )),
218 Network::Regtest => Arc::new(RestClientChainService::new(
219 "https://regtest-mempool.us-west-2.sparkinfra.net/api".to_string(),
220 self.config.network,
221 5,
222 Box::new(inner_client),
223 match (
224 std::env::var("CHAIN_SERVICE_USERNAME"),
225 std::env::var("CHAIN_SERVICE_PASSWORD"),
226 ) {
227 (Ok(username), Ok(password)) => Some(BasicAuth::new(username, password)),
228 _ => Some(BasicAuth::new(
229 "spark-sdk".to_string(),
230 "mCMk1JqlBNtetUNy".to_string(),
231 )),
232 },
233 ChainApiType::MempoolSpace,
234 )),
235 }
236 };
237
238 let (storage, sync_storage) = match (self.storage, self.storage_dir) {
239 (Some(storage), _) => (storage, self.sync_storage),
241 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
243 (None, Some(storage_dir)) => {
244 let identity_pub_key = signer
245 .get_identity_public_key()
246 .map_err(|e| SdkError::Generic(e.to_string()))?;
247 let storage =
248 default_storage(&storage_dir, self.config.network, &identity_pub_key)?;
249 let sync_storage = match (self.sync_storage, &self.config.real_time_sync_server_url)
250 {
251 (Some(sync_storage), _) => Some(sync_storage),
253 (None, Some(_)) => Some(default_sync_storage(
256 &storage_dir,
257 self.config.network,
258 &identity_pub_key,
259 )?),
260 _ => None,
261 };
262 (storage, sync_storage)
263 }
264 _ => {
265 return Err(SdkError::Generic(
266 "Either storage or storage_dir must be set before building the SDK".to_string(),
267 ));
268 }
269 };
270
271 let fiat_service: Arc<dyn breez_sdk_common::fiat::FiatService> = match self.fiat_service {
272 Some(service) => Arc::new(FiatServiceWrapper::new(service)),
273 None => Arc::new(
274 BreezServer::new(PRODUCTION_BREEZSERVER_URL, None)
275 .map_err(|e| SdkError::Generic(e.to_string()))?,
276 ),
277 };
278
279 let lnurl_client: Arc<dyn breez_sdk_common::rest::RestClient> = match self.lnurl_client {
280 Some(client) => Arc::new(RestClientWrapper::new(client)),
281 None => Arc::new(
282 CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?,
283 ),
284 };
285 let user_agent = format!(
286 "{}/{}",
287 crate::built_info::PKG_NAME,
288 crate::built_info::GIT_VERSION.unwrap_or(crate::built_info::PKG_VERSION),
289 );
290 info!("Building SparkWallet with user agent: {}", user_agent);
291 let mut spark_wallet_config =
292 spark_wallet::SparkWalletConfig::default_config(self.config.network.into());
293 spark_wallet_config.operator_pool = spark_wallet_config
294 .operator_pool
295 .with_user_agent(Some(user_agent.clone()));
296 spark_wallet_config.service_provider_config.user_agent = Some(user_agent);
297
298 let mut wallet_builder =
299 spark_wallet::WalletBuilder::new(spark_wallet_config, Arc::clone(&signer));
300 if let Some(observer) = self.payment_observer {
301 let observer: Arc<dyn spark_wallet::TransferObserver> =
302 Arc::new(SparkTransferObserver::new(observer));
303 wallet_builder = wallet_builder.with_transfer_observer(observer);
304 }
305 let spark_wallet = Arc::new(wallet_builder.build().await?);
306
307 let lnurl_server_client: Option<Arc<dyn LnurlServerClient>> = match self.lnurl_server_client
308 {
309 Some(client) => Some(client),
310 None => match &self.config.lnurl_domain {
311 Some(domain) => {
312 Some(Arc::new(ReqwestLnurlServerClient::new(
314 domain.clone(),
315 self.config.api_key.clone(),
316 Arc::clone(&spark_wallet),
317 )?))
318 }
319 None => None,
320 },
321 };
322 let shutdown_sender = watch::channel::<()>(()).0;
323
324 let event_emitter = Arc::new(EventEmitter::new(
325 self.config.real_time_sync_server_url.is_some(),
326 ));
327 let storage = if let Some(server_url) = &self.config.real_time_sync_server_url {
328 let Some(sync_storage) = sync_storage else {
329 return Err(SdkError::Generic(
330 "Real-time sync is enabled, but no sync storage is supplied".to_string(),
331 ));
332 };
333
334 let master_key =
336 if self.key_set_type == KeySetType::Default && self.account_number.is_none() {
337 let bitcoin_network: bitcoin::Network = self.config.network.into();
338 Xpriv::new_master(bitcoin_network, &seed_bytes)
339 .map_err(|e| SdkError::Generic(e.to_string()))?
340 } else {
341 key_set.identity_master_key
342 };
343
344 init_and_start_real_time_sync(RealTimeSyncParams {
345 server_url: server_url.clone(),
346 api_key: self.config.api_key.clone(),
347 network: self.config.network,
348 master_key,
349 storage: Arc::clone(&storage),
350 sync_storage,
351 shutdown_receiver: shutdown_sender.subscribe(),
352 event_emitter: Arc::clone(&event_emitter),
353 })
354 .await?
355 } else {
356 storage
357 };
358
359 let nostr_client = Arc::new(NostrClient::new(
360 &key_set.identity_master_key,
361 self.account_number.unwrap_or(match self.config.network {
362 Network::Mainnet => 0,
363 Network::Regtest => 1,
364 }),
365 )?);
366
367 let sdk = BreezSdk::init_and_start(BreezSdkParams {
369 config: self.config,
370 storage,
371 chain_service,
372 fiat_service,
373 lnurl_client,
374 lnurl_server_client,
375 shutdown_sender,
376 spark_wallet,
377 event_emitter,
378 nostr_client,
379 })?;
380 debug!("Initialized and started breez sdk.");
381
382 Ok(sdk)
383 }
384}
385
386#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
387fn default_storage(
388 data_dir: &str,
389 network: Network,
390 identity_pub_key: &spark_wallet::PublicKey,
391) -> Result<Arc<dyn Storage>, SdkError> {
392 let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
393 let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
394 Ok(storage)
395}
396
397#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
398fn default_sync_storage(
399 data_dir: &str,
400 network: Network,
401 identity_pub_key: &spark_wallet::PublicKey,
402) -> Result<Arc<dyn SyncStorage>, SdkError> {
403 let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
404 let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
405 Ok(storage)
406}