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 breez_sdk_common::{
8    breez_server::{BreezServer, PRODUCTION_BREEZSERVER_URL},
9    rest::ReqwestRestClient as CommonRequestRestClient,
10};
11use spark_wallet::{DefaultSigner, Signer};
12use tokio::sync::watch;
13use tracing::debug;
14
15use crate::{
16    Credentials, EventEmitter, FiatService, FiatServiceWrapper, KeySetType, Network, RestClient,
17    RestClientWrapper, Seed,
18    chain::{
19        BitcoinChainService,
20        rest_client::{BasicAuth, RestClientChainService},
21    },
22    error::SdkError,
23    lnurl::{LnurlServerClient, ReqwestLnurlServerClient},
24    models::Config,
25    payment_observer::{PaymentObserver, SparkTransferObserver},
26    persist::Storage,
27    realtime_sync::{RealTimeSyncParams, init_and_start_real_time_sync},
28    sdk::{BreezSdk, BreezSdkParams},
29    sync_storage::SyncStorage,
30};
31
32/// Builder for creating `BreezSdk` instances with customizable components.
33#[derive(Clone)]
34pub struct SdkBuilder {
35    config: Config,
36    seed: Seed,
37    storage_dir: Option<String>,
38    storage: Option<Arc<dyn Storage>>,
39    chain_service: Option<Arc<dyn BitcoinChainService>>,
40    fiat_service: Option<Arc<dyn FiatService>>,
41    lnurl_client: Option<Arc<dyn RestClient>>,
42    lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
43    payment_observer: Option<Arc<dyn PaymentObserver>>,
44    key_set_type: KeySetType,
45    use_address_index: bool,
46    account_number: Option<u32>,
47    sync_storage: Option<Arc<dyn SyncStorage>>,
48}
49
50impl SdkBuilder {
51    /// Creates a new `SdkBuilder` with the provided configuration.
52    /// Arguments:
53    /// - `config`: The configuration to be used.
54    /// - `seed`: The seed for wallet generation.
55    pub fn new(config: Config, seed: Seed) -> Self {
56        SdkBuilder {
57            config,
58            seed,
59            storage_dir: None,
60            storage: None,
61            chain_service: None,
62            fiat_service: None,
63            lnurl_client: None,
64            lnurl_server_client: None,
65            payment_observer: None,
66            key_set_type: KeySetType::Default,
67            use_address_index: false,
68            account_number: None,
69            sync_storage: None,
70        }
71    }
72
73    #[must_use]
74    /// Sets the root storage directory to initialize the default storage with.
75    /// This initializes both storage and real-time sync storage with the
76    /// default implementations.
77    /// Arguments:
78    /// - `storage_dir`: The data directory for storage.
79    pub fn with_default_storage(mut self, storage_dir: String) -> Self {
80        self.storage_dir = Some(storage_dir);
81        self
82    }
83
84    #[must_use]
85    /// Sets the storage implementation to be used by the SDK.
86    /// Arguments:
87    /// - `storage`: The storage implementation to be used.
88    pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
89        self.storage = Some(storage);
90        self
91    }
92
93    #[must_use]
94    /// Sets the real-time sync storage implementation to be used by the SDK.
95    /// Arguments:
96    /// - `storage`: The sync storage implementation to be used.
97    pub fn with_real_time_sync_storage(mut self, storage: Arc<dyn SyncStorage>) -> Self {
98        self.sync_storage = Some(storage);
99        self
100    }
101
102    /// Sets the key set type to be used by the SDK.
103    /// Arguments:
104    /// - `key_set_type`: The key set type which determines the derivation path.
105    /// - `use_address_index`: Controls the structure of the BIP derivation path.
106    #[must_use]
107    pub fn with_key_set(
108        mut self,
109        key_set_type: KeySetType,
110        use_address_index: bool,
111        account_number: Option<u32>,
112    ) -> Self {
113        self.key_set_type = key_set_type;
114        self.use_address_index = use_address_index;
115        self.account_number = account_number;
116        self
117    }
118
119    /// Sets the chain service to be used by the SDK.
120    /// Arguments:
121    /// - `chain_service`: The chain service to be used.
122    #[must_use]
123    pub fn with_chain_service(mut self, chain_service: Arc<dyn BitcoinChainService>) -> Self {
124        self.chain_service = Some(chain_service);
125        self
126    }
127
128    /// Sets the REST chain service to be used by the SDK.
129    /// Arguments:
130    /// - `url`: The base URL of the REST API.
131    /// - `credentials`: Optional credentials for basic authentication.
132    #[must_use]
133    pub fn with_rest_chain_service(
134        mut self,
135        url: String,
136        credentials: Option<Credentials>,
137    ) -> Self {
138        self.chain_service = Some(Arc::new(RestClientChainService::new(
139            url,
140            self.config.network,
141            5,
142            Box::new(CommonRequestRestClient::new().unwrap()),
143            credentials.map(|c| BasicAuth::new(c.username, c.password)),
144        )));
145        self
146    }
147
148    /// Sets the fiat service to be used by the SDK.
149    /// Arguments:
150    /// - `fiat_service`: The fiat service to be used.
151    #[must_use]
152    pub fn with_fiat_service(mut self, fiat_service: Arc<dyn FiatService>) -> Self {
153        self.fiat_service = Some(fiat_service);
154        self
155    }
156
157    #[must_use]
158    pub fn with_lnurl_client(mut self, lnurl_client: Arc<dyn RestClient>) -> Self {
159        self.lnurl_client = Some(lnurl_client);
160        self
161    }
162
163    #[must_use]
164    #[allow(unused)]
165    pub fn with_lnurl_server_client(
166        mut self,
167        lnurl_serverclient: Arc<dyn LnurlServerClient>,
168    ) -> Self {
169        self.lnurl_server_client = Some(lnurl_serverclient);
170        self
171    }
172
173    /// Sets the payment observer to be used by the SDK.
174    /// This observer will receive callbacks before outgoing payments for Lightning, Spark and onchain Bitcoin.
175    /// Arguments:
176    /// - `payment_observer`: The payment observer to be used.
177    #[must_use]
178    #[allow(unused)]
179    pub fn with_payment_observer(mut self, payment_observer: Arc<dyn PaymentObserver>) -> Self {
180        self.payment_observer = Some(payment_observer);
181        self
182    }
183
184    /// Builds the `BreezSdk` instance with the configured components.
185    #[allow(clippy::too_many_lines)]
186    pub async fn build(self) -> Result<BreezSdk, SdkError> {
187        let (storage, sync_storage) = match (self.storage, self.storage_dir) {
188            // Use provided storages directly
189            (Some(storage), _) => (storage, self.sync_storage),
190            // Initialize default storages based on provided directory
191            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
192            (None, Some(storage_dir)) => {
193                let storage = default_storage(&storage_dir, self.config.network, &self.seed)?;
194                let sync_storage = match (self.sync_storage, &self.config.real_time_sync_server_url)
195                {
196                    // Use provided sync storage directly
197                    (Some(sync_storage), _) => Some(sync_storage),
198                    // Initialize default sync storage based on provided directory
199                    // if real-time sync is enabled
200                    (None, Some(_)) => Some(default_sync_storage(
201                        &storage_dir,
202                        self.config.network,
203                        &self.seed,
204                    )?),
205                    _ => None,
206                };
207                (storage, sync_storage)
208            }
209            _ => {
210                return Err(SdkError::Generic(
211                    "Either storage or storage_dir must be set before building the SDK".to_string(),
212                ));
213            }
214        };
215        // Create the signer from seed
216        let seed = match self.seed {
217            Seed::Mnemonic {
218                mnemonic,
219                passphrase,
220            } => {
221                let mnemonic = bip39::Mnemonic::parse(&mnemonic)
222                    .map_err(|e| SdkError::Generic(e.to_string()))?;
223
224                mnemonic
225                    .to_seed(passphrase.as_deref().unwrap_or(""))
226                    .to_vec()
227            }
228            Seed::Entropy(entropy) => entropy,
229        };
230
231        let signer: Arc<dyn Signer> = Arc::new(
232            DefaultSigner::with_keyset_type(
233                &seed,
234                self.config.network.into(),
235                self.key_set_type.into(),
236                self.use_address_index,
237                self.account_number,
238            )
239            .map_err(|e| SdkError::Generic(e.to_string()))?,
240        );
241        let chain_service = if let Some(service) = self.chain_service {
242            service
243        } else {
244            let inner_client =
245                CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?;
246            match self.config.network {
247                Network::Mainnet => Arc::new(RestClientChainService::new(
248                    "https://blockstream.info/api".to_string(),
249                    self.config.network,
250                    5,
251                    Box::new(inner_client),
252                    None,
253                )),
254                Network::Regtest => Arc::new(RestClientChainService::new(
255                    "https://regtest-mempool.us-west-2.sparkinfra.net/api".to_string(),
256                    self.config.network,
257                    5,
258                    Box::new(inner_client),
259                    match (
260                        std::env::var("CHAIN_SERVICE_USERNAME"),
261                        std::env::var("CHAIN_SERVICE_PASSWORD"),
262                    ) {
263                        (Ok(username), Ok(password)) => Some(BasicAuth::new(username, password)),
264                        _ => Some(BasicAuth::new(
265                            "spark-sdk".to_string(),
266                            "mCMk1JqlBNtetUNy".to_string(),
267                        )),
268                    },
269                )),
270            }
271        };
272
273        let fiat_service: Arc<dyn breez_sdk_common::fiat::FiatService> = match self.fiat_service {
274            Some(service) => Arc::new(FiatServiceWrapper::new(service)),
275            None => Arc::new(
276                BreezServer::new(PRODUCTION_BREEZSERVER_URL, None)
277                    .map_err(|e| SdkError::Generic(e.to_string()))?,
278            ),
279        };
280
281        let lnurl_client: Arc<dyn breez_sdk_common::rest::RestClient> = match self.lnurl_client {
282            Some(client) => Arc::new(RestClientWrapper::new(client)),
283            None => Arc::new(
284                CommonRequestRestClient::new().map_err(|e| SdkError::Generic(e.to_string()))?,
285            ),
286        };
287        let spark_wallet_config =
288            spark_wallet::SparkWalletConfig::default_config(self.config.network.into());
289
290        let mut wallet_builder =
291            spark_wallet::WalletBuilder::new(spark_wallet_config, Arc::clone(&signer));
292        if let Some(observer) = self.payment_observer {
293            let observer: Arc<dyn spark_wallet::TransferObserver> =
294                Arc::new(SparkTransferObserver::new(observer));
295            wallet_builder = wallet_builder.with_transfer_observer(observer);
296        }
297        let spark_wallet = Arc::new(wallet_builder.build().await?);
298
299        let lnurl_server_client: Option<Arc<dyn LnurlServerClient>> = match self.lnurl_server_client
300        {
301            Some(client) => Some(client),
302            None => match &self.config.lnurl_domain {
303                Some(domain) => {
304                    // Get the SparkWallet instance for signing
305                    Some(Arc::new(ReqwestLnurlServerClient::new(
306                        domain.clone(),
307                        self.config.api_key.clone(),
308                        Arc::clone(&spark_wallet),
309                    )?))
310                }
311                None => None,
312            },
313        };
314        let shutdown_sender = watch::channel::<()>(()).0;
315
316        let event_emitter = Arc::new(EventEmitter::new());
317        let storage = if let Some(server_url) = &self.config.real_time_sync_server_url {
318            let Some(sync_storage) = sync_storage else {
319                return Err(SdkError::Generic(
320                    "Real-time sync is enabled, but no sync storage is supplied".to_string(),
321                ));
322            };
323            init_and_start_real_time_sync(RealTimeSyncParams {
324                server_url: server_url.clone(),
325                api_key: self.config.api_key.clone(),
326                network: self.config.network,
327                seed,
328                storage: Arc::clone(&storage),
329                sync_storage,
330                shutdown_receiver: shutdown_sender.subscribe(),
331                event_emitter: Arc::clone(&event_emitter),
332            })
333            .await?
334        } else {
335            storage
336        };
337
338        // Create the SDK instance
339        let sdk = BreezSdk::init_and_start(BreezSdkParams {
340            config: self.config,
341            storage,
342            chain_service,
343            fiat_service,
344            lnurl_client,
345            lnurl_server_client,
346            shutdown_sender,
347            spark_wallet,
348            event_emitter,
349        })?;
350        debug!("Initialized and started breez sdk.");
351
352        Ok(sdk)
353    }
354}
355
356#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
357fn default_storage(
358    data_dir: &str,
359    network: Network,
360    seed: &Seed,
361) -> Result<Arc<dyn Storage>, SdkError> {
362    let db_path = crate::default_storage_path(data_dir, &network, seed)?;
363    let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
364    Ok(storage)
365}
366
367#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
368fn default_sync_storage(
369    data_dir: &str,
370    network: Network,
371    seed: &Seed,
372) -> Result<Arc<dyn SyncStorage>, SdkError> {
373    let db_path = crate::default_storage_path(data_dir, &network, seed)?;
374    let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
375    Ok(storage)
376}