breez_sdk_spark/
sdk_builder.rs

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/// Builder for creating `BreezSdk` instances with customizable components.
35#[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    /// Creates a new `SdkBuilder` with the provided configuration.
54    /// Arguments:
55    /// - `config`: The configuration to be used.
56    /// - `seed`: The seed for wallet generation.
57    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    /// Sets the root storage directory to initialize the default storage with.
77    /// This initializes both storage and real-time sync storage with the
78    /// default implementations.
79    /// Arguments:
80    /// - `storage_dir`: The data directory for storage.
81    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    /// Sets the storage implementation to be used by the SDK.
88    /// Arguments:
89    /// - `storage`: The storage implementation to be used.
90    pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
91        self.storage = Some(storage);
92        self
93    }
94
95    #[must_use]
96    /// Sets the real-time sync storage implementation to be used by the SDK.
97    /// Arguments:
98    /// - `storage`: The sync storage implementation to be used.
99    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    /// Sets the key set type to be used by the SDK.
105    /// Arguments:
106    /// - `key_set_type`: The key set type which determines the derivation path.
107    /// - `use_address_index`: Controls the structure of the BIP derivation path.
108    #[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    /// Sets the chain service to be used by the SDK.
122    /// Arguments:
123    /// - `chain_service`: The chain service to be used.
124    #[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    /// Sets the REST chain service to be used by the SDK.
131    /// Arguments:
132    /// - `url`: The base URL of the REST API.
133    /// - `api_type`: The API type to be used.
134    /// - `credentials`: Optional credentials for basic authentication.
135    #[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    /// Sets the fiat service to be used by the SDK.
154    /// Arguments:
155    /// - `fiat_service`: The fiat service to be used.
156    #[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    /// Sets the payment observer to be used by the SDK.
179    /// This observer will receive callbacks before outgoing payments for Lightning, Spark and onchain Bitcoin.
180    /// Arguments:
181    /// - `payment_observer`: The payment observer to be used.
182    #[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    /// Builds the `BreezSdk` instance with the configured components.
190    #[allow(clippy::too_many_lines)]
191    pub async fn build(self) -> Result<BreezSdk, SdkError> {
192        // Create the signer from seed
193        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            // Use provided storages directly
240            (Some(storage), _) => (storage, self.sync_storage),
241            // Initialize default storages based on provided directory
242            #[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                    // Use provided sync storage directly
252                    (Some(sync_storage), _) => Some(sync_storage),
253                    // Initialize default sync storage based on provided directory
254                    // if real-time sync is enabled
255                    (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                    // Get the SparkWallet instance for signing
313                    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            // Use legacy master key when using default key set and default derivation path for backwards compatibility
335            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        // Create the SDK instance
368        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}