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    buy::moonpay::MoonpayProvider,
10};
11use platform_utils::DefaultHttpClient;
12
13#[cfg(not(target_family = "wasm"))]
14use spark_wallet::Signer;
15use spark_wallet::{SparkWalletConfig, TokenOutputStore, TreeStore};
16use tokio::sync::watch;
17use tracing::{debug, info};
18
19use flashnet::{FlashnetConfig, IntegratorConfig};
20
21use crate::{
22    Credentials, EventEmitter, FiatService, FiatServiceWrapper, KeySetType, Network, Seed,
23    chain::{
24        BitcoinChainService,
25        rest_client::{BasicAuth, ChainApiType, RestClientChainService},
26    },
27    error::SdkError,
28    lnurl::{DefaultLnurlServerClient, LnurlServerClient},
29    models::Config,
30    payment_observer::{PaymentObserver, SparkTransferObserver},
31    persist::Storage,
32    realtime_sync::{RealTimeSyncParams, init_and_start_real_time_sync},
33    sdk::{BreezSdk, BreezSdkParams, SyncCoordinator},
34    signer::{
35        breez::BreezSignerImpl, lnurl_auth::LnurlAuthSignerAdapter, rtsync::RTSyncSigner,
36        spark::SparkSigner,
37    },
38    stable_balance::StableBalance,
39    token_conversion::TokenConversionMiddleware,
40    token_conversion::{
41        DEFAULT_INTEGRATOR_FEE_BPS, DEFAULT_INTEGRATOR_PUBKEY, FlashnetTokenConverter,
42        TokenConverter,
43    },
44};
45
46/// Source for the signer - either a seed or an external signer implementation
47#[derive(Clone)]
48enum SignerSource {
49    Seed {
50        seed: Seed,
51        key_set_type: KeySetType,
52        use_address_index: bool,
53        account_number: Option<u32>,
54    },
55    External(Arc<dyn crate::signer::ExternalSigner>),
56}
57
58/// Builder for creating `BreezSdk` instances with customizable components.
59#[derive(Clone)]
60pub struct SdkBuilder {
61    config: Config,
62    signer_source: SignerSource,
63
64    storage_dir: Option<String>,
65    storage: Option<Arc<dyn Storage>>,
66    #[cfg(feature = "postgres")]
67    postgres_backend_config: Option<crate::persist::postgres::PostgresStorageConfig>,
68    chain_service: Option<Arc<dyn BitcoinChainService>>,
69    fiat_service: Option<Arc<dyn FiatService>>,
70    lnurl_client: Option<Arc<dyn platform_utils::HttpClient>>,
71    lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
72    payment_observer: Option<Arc<dyn PaymentObserver>>,
73    tree_store: Option<Arc<dyn TreeStore>>,
74    token_output_store: Option<Arc<dyn TokenOutputStore>>,
75}
76
77impl SdkBuilder {
78    /// Creates a new `SdkBuilder` with the provided configuration and seed.
79    ///
80    /// For external signer support, use `new_with_signer` instead.
81    ///
82    /// # Arguments
83    /// - `config`: The configuration to be used.
84    /// - `seed`: The seed for wallet generation.
85    #[allow(clippy::needless_pass_by_value)]
86    pub fn new(config: Config, seed: Seed) -> Self {
87        SdkBuilder {
88            config,
89            signer_source: SignerSource::Seed {
90                seed,
91                key_set_type: KeySetType::Default,
92                use_address_index: false,
93                account_number: None,
94            },
95            storage_dir: None,
96            storage: None,
97            #[cfg(feature = "postgres")]
98            postgres_backend_config: None,
99            chain_service: None,
100            fiat_service: None,
101            lnurl_client: None,
102            lnurl_server_client: None,
103            payment_observer: None,
104            tree_store: None,
105            token_output_store: None,
106        }
107    }
108
109    /// Creates a new `SdkBuilder` with the provided configuration and external signer.
110    ///
111    /// # Arguments
112    /// - `config`: The configuration to be used.
113    /// - `signer`: An external signer implementation.
114    #[allow(clippy::needless_pass_by_value)]
115    pub fn new_with_signer(config: Config, signer: Arc<dyn crate::signer::ExternalSigner>) -> Self {
116        SdkBuilder {
117            config,
118            signer_source: SignerSource::External(signer),
119            storage_dir: None,
120            storage: None,
121            #[cfg(feature = "postgres")]
122            postgres_backend_config: None,
123            chain_service: None,
124            fiat_service: None,
125            lnurl_client: None,
126            lnurl_server_client: None,
127            payment_observer: None,
128            tree_store: None,
129            token_output_store: None,
130        }
131    }
132
133    /// Sets the key set type to be used by the SDK.
134    ///
135    /// Note: This only applies when using a seed-based signer. It has no effect
136    /// when using an external signer (created with `new_with_signer`).
137    ///
138    /// # Arguments
139    /// - `config`: Key set configuration containing the key set type, address index flag, and optional account number.
140    #[must_use]
141    pub fn with_key_set(mut self, config: crate::models::KeySetConfig) -> Self {
142        if let SignerSource::Seed {
143            key_set_type: ref mut kst,
144            use_address_index: ref mut uai,
145            account_number: ref mut an,
146            ..
147        } = self.signer_source
148        {
149            *kst = config.key_set_type;
150            *uai = config.use_address_index;
151            *an = config.account_number;
152        }
153        self
154    }
155
156    #[must_use]
157    /// Sets the root storage directory to initialize the default storage with.
158    /// This initializes both storage and real-time sync storage with the
159    /// default implementations.
160    /// Arguments:
161    /// - `storage_dir`: The data directory for storage.
162    pub fn with_default_storage(mut self, storage_dir: String) -> Self {
163        self.storage_dir = Some(storage_dir);
164        self
165    }
166
167    #[must_use]
168    /// Sets the storage implementation to be used by the SDK.
169    /// Arguments:
170    /// - `storage`: The storage implementation to be used.
171    pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
172        self.storage = Some(storage);
173        self
174    }
175
176    /// Sets `PostgreSQL` as the backend for all stores (storage, tree store, and token store).
177    /// The store instances will be created during `build()`.
178    /// Arguments:
179    /// - `config`: The `PostgreSQL` storage configuration.
180    #[must_use]
181    #[cfg(feature = "postgres")]
182    pub fn with_postgres_backend(
183        mut self,
184        config: crate::persist::postgres::PostgresStorageConfig,
185    ) -> Self {
186        self.postgres_backend_config = Some(config);
187        self
188    }
189
190    /// Sets the chain service to be used by the SDK.
191    /// Arguments:
192    /// - `chain_service`: The chain service to be used.
193    #[must_use]
194    pub fn with_chain_service(mut self, chain_service: Arc<dyn BitcoinChainService>) -> Self {
195        self.chain_service = Some(chain_service);
196        self
197    }
198
199    /// Sets the REST chain service to be used by the SDK.
200    /// Arguments:
201    /// - `url`: The base URL of the REST API.
202    /// - `api_type`: The API type to be used.
203    /// - `credentials`: Optional credentials for basic authentication.
204    #[must_use]
205    pub fn with_rest_chain_service(
206        mut self,
207        url: String,
208        api_type: ChainApiType,
209        credentials: Option<Credentials>,
210    ) -> Self {
211        self.chain_service = Some(Arc::new(RestClientChainService::new(
212            url,
213            self.config.network,
214            5,
215            Box::new(DefaultHttpClient::default()),
216            credentials.map(|c| BasicAuth::new(c.username, c.password)),
217            api_type,
218        )));
219        self
220    }
221
222    /// Sets the fiat service to be used by the SDK.
223    /// Arguments:
224    /// - `fiat_service`: The fiat service to be used.
225    #[must_use]
226    pub fn with_fiat_service(mut self, fiat_service: Arc<dyn FiatService>) -> Self {
227        self.fiat_service = Some(fiat_service);
228        self
229    }
230
231    #[must_use]
232    pub fn with_lnurl_client(mut self, lnurl_client: Arc<dyn crate::RestClient>) -> Self {
233        self.lnurl_client = Some(Arc::new(crate::common::rest::RestClientWrapper::new(
234            lnurl_client,
235        )));
236        self
237    }
238
239    #[must_use]
240    #[allow(unused)]
241    pub fn with_lnurl_server_client(
242        mut self,
243        lnurl_serverclient: Arc<dyn LnurlServerClient>,
244    ) -> Self {
245        self.lnurl_server_client = Some(lnurl_serverclient);
246        self
247    }
248
249    /// Sets the payment observer to be used by the SDK.
250    /// This observer will receive callbacks before outgoing payments for Lightning, Spark and onchain Bitcoin.
251    /// Arguments:
252    /// - `payment_observer`: The payment observer to be used.
253    #[must_use]
254    #[allow(unused)]
255    pub fn with_payment_observer(mut self, payment_observer: Arc<dyn PaymentObserver>) -> Self {
256        self.payment_observer = Some(payment_observer);
257        self
258    }
259
260    /// Sets a custom tree store implementation.
261    ///
262    /// # Arguments
263    /// - `tree_store`: The tree store implementation to use.
264    #[must_use]
265    pub fn with_tree_store(mut self, tree_store: Arc<dyn TreeStore>) -> Self {
266        self.tree_store = Some(tree_store);
267        self
268    }
269
270    /// Sets a custom token output store implementation.
271    ///
272    /// # Arguments
273    /// - `token_output_store`: The token output store implementation to use.
274    #[must_use]
275    pub fn with_token_output_store(
276        mut self,
277        token_output_store: Arc<dyn TokenOutputStore>,
278    ) -> Self {
279        self.token_output_store = Some(token_output_store);
280        self
281    }
282
283    /// Builds a [`SparkWalletConfig`](spark_wallet::SparkWalletConfig) from a
284    /// [`SparkConfig`](crate::models::SparkConfig).
285    fn build_spark_wallet_config(
286        network: spark_wallet::Network,
287        env_config: &crate::models::SparkConfig,
288    ) -> Result<spark_wallet::SparkWalletConfig, SdkError> {
289        let coordinator_index = env_config
290            .signing_operators
291            .iter()
292            .position(|op| op.identifier == env_config.coordinator_identifier)
293            .ok_or_else(|| {
294                SdkError::InvalidInput(
295                    "coordinator_identifier does not match any signing operator".to_string(),
296                )
297            })?;
298
299        let operators: Vec<_> = env_config
300            .signing_operators
301            .iter()
302            .map(|op| {
303                SparkWalletConfig::create_operator_config(
304                    op.id as usize,
305                    &op.identifier,
306                    &op.address,
307                    None,
308                    &op.identity_public_key,
309                )
310                .map_err(|e| SdkError::InvalidInput(e.to_string()))
311            })
312            .collect::<Result<_, _>>()?;
313
314        let operator_pool = spark_wallet::OperatorPoolConfig::new(coordinator_index, operators)
315            .map_err(|e| SdkError::InvalidInput(e.to_string()))?;
316
317        let service_provider_config = SparkWalletConfig::create_service_provider_config(
318            &env_config.ssp_config.base_url,
319            &env_config.ssp_config.identity_public_key,
320            env_config.ssp_config.schema_endpoint.clone(),
321        )
322        .map_err(|e| SdkError::InvalidInput(e.to_string()))?;
323
324        let mut config = SparkWalletConfig::default_config(network);
325        config.operator_pool = operator_pool;
326        config.split_secret_threshold = env_config.threshold;
327        config.service_provider_config = service_provider_config;
328        config.tokens_config.expected_withdraw_bond_sats = env_config.expected_withdraw_bond_sats;
329        config
330            .tokens_config
331            .expected_withdraw_relative_block_locktime =
332            env_config.expected_withdraw_relative_block_locktime;
333
334        Ok(config)
335    }
336
337    /// Builds the `BreezSdk` instance with the configured components.
338    #[allow(clippy::too_many_lines)]
339    pub async fn build(self) -> Result<BreezSdk, SdkError> {
340        // Validate configuration
341        self.config.validate()?;
342
343        // Create the base signer based on the signer source
344        let signer: Arc<dyn crate::signer::BreezSigner> = match self.signer_source {
345            SignerSource::Seed {
346                seed,
347                key_set_type,
348                use_address_index,
349                account_number,
350            } => Arc::new(
351                BreezSignerImpl::new(
352                    &self.config,
353                    &seed,
354                    key_set_type.into(),
355                    use_address_index,
356                    account_number,
357                )
358                .map_err(|e| SdkError::Generic(e.to_string()))?,
359            ),
360            SignerSource::External(external_signer) => {
361                use crate::signer::ExternalSignerAdapter;
362                Arc::new(ExternalSignerAdapter::new(external_signer))
363            }
364        };
365
366        // Create the specialized signers
367        let spark_signer = Arc::new(SparkSigner::new(signer.clone()));
368        let rtsync_signer = Arc::new(
369            RTSyncSigner::new(signer.clone(), self.config.network)
370                .map_err(|e| SdkError::Generic(e.to_string()))?,
371        );
372        let lnurl_auth_signer = Arc::new(LnurlAuthSignerAdapter::new(signer.clone()));
373
374        let chain_service = if let Some(service) = self.chain_service {
375            service
376        } else {
377            let inner_client = DefaultHttpClient::default();
378            match self.config.network {
379                Network::Mainnet => Arc::new(RestClientChainService::new(
380                    "https://blockstream.info/api".to_string(),
381                    self.config.network,
382                    5,
383                    Box::new(inner_client),
384                    None,
385                    ChainApiType::Esplora,
386                )),
387                Network::Regtest => Arc::new(RestClientChainService::new(
388                    "https://regtest-mempool.us-west-2.sparkinfra.net/api".to_string(),
389                    self.config.network,
390                    5,
391                    Box::new(inner_client),
392                    match (
393                        std::env::var("CHAIN_SERVICE_USERNAME"),
394                        std::env::var("CHAIN_SERVICE_PASSWORD"),
395                    ) {
396                        (Ok(username), Ok(password)) => Some(BasicAuth::new(username, password)),
397                        _ => Some(BasicAuth::new(
398                            "spark-sdk".to_string(),
399                            "mCMk1JqlBNtetUNy".to_string(),
400                        )),
401                    },
402                    ChainApiType::MempoolSpace,
403                )),
404            }
405        };
406
407        // Validate storage configuration
408        #[cfg(feature = "postgres")]
409        let has_postgres = self.postgres_backend_config.is_some();
410        #[cfg(not(feature = "postgres"))]
411        let has_postgres = false;
412
413        let storage_count = [
414            self.storage.is_some(),
415            self.storage_dir.is_some(),
416            has_postgres,
417        ]
418        .into_iter()
419        .filter(|&v| v)
420        .count();
421        match storage_count {
422            0 => return Err(SdkError::Generic("No storage configured".to_string())),
423            2.. => {
424                return Err(SdkError::Generic(
425                    "Multiple storage configurations provided".to_string(),
426                ));
427            }
428            _ => {}
429        }
430
431        // Create a shared PostgreSQL pool if postgres backend is configured.
432        // This single pool is reused for storage, tree store, and token store.
433        #[cfg(feature = "postgres")]
434        let postgres_pool = if let Some(ref postgres_config) = self.postgres_backend_config {
435            Some(
436                crate::persist::postgres::create_pool(postgres_config)
437                    .map_err(|e| SdkError::Generic(e.to_string()))?,
438            )
439        } else {
440            None
441        };
442
443        // Initialize storage
444        let storage: Arc<dyn Storage> = if let Some(storage) = self.storage {
445            storage
446        } else if let Some(storage_dir) = self.storage_dir {
447            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
448            {
449                let identity_pub_key = spark_signer
450                    .get_identity_public_key()
451                    .await
452                    .map_err(|e| SdkError::Generic(e.to_string()))?;
453                default_storage(&storage_dir, self.config.network, &identity_pub_key)?
454            }
455            #[cfg(all(target_family = "wasm", target_os = "unknown"))]
456            {
457                let _ = storage_dir;
458                return Err(SdkError::Generic(
459                    "with_default_storage is not supported on WASM".to_string(),
460                ));
461            }
462        } else {
463            #[cfg(all(
464                feature = "postgres",
465                not(all(target_family = "wasm", target_os = "unknown"))
466            ))]
467            if let Some(ref pool) = postgres_pool {
468                Arc::new(
469                    crate::persist::postgres::PostgresStorage::new_with_pool(pool.clone())
470                        .await
471                        .map_err(|e| SdkError::Generic(e.to_string()))?,
472                )
473            } else {
474                return Err(SdkError::Generic("No storage configured".to_string()));
475            }
476            #[cfg(not(all(
477                feature = "postgres",
478                not(all(target_family = "wasm", target_os = "unknown"))
479            )))]
480            {
481                return Err(SdkError::Generic("No storage configured".to_string()));
482            }
483        };
484
485        let user_agent = format!(
486            "{}/{}",
487            crate::built_info::PKG_NAME,
488            crate::built_info::GIT_VERSION.unwrap_or(crate::built_info::PKG_VERSION),
489        );
490        info!("Building sdk with user agent: {}", user_agent);
491
492        let breez_server = Arc::new(
493            BreezServer::new(PRODUCTION_BREEZSERVER_URL, None, &user_agent)
494                .map_err(|e| SdkError::Generic(e.to_string()))?,
495        );
496
497        let fiat_service: Arc<dyn breez_sdk_common::fiat::FiatService> = match self.fiat_service {
498            Some(service) => Arc::new(FiatServiceWrapper::new(service)),
499            None => breez_server.clone(),
500        };
501
502        let lnurl_client: Arc<dyn platform_utils::HttpClient> = match self.lnurl_client {
503            Some(client) => client,
504            None => Arc::new(DefaultHttpClient::default()),
505        };
506        let mut spark_wallet_config = if let Some(env_config) = &self.config.spark_config {
507            Self::build_spark_wallet_config(self.config.network.into(), env_config)?
508        } else {
509            spark_wallet::SparkWalletConfig::default_config(self.config.network.into())
510        };
511        spark_wallet_config.operator_pool = spark_wallet_config
512            .operator_pool
513            .with_user_agent(Some(user_agent.clone()));
514        spark_wallet_config.service_provider_config.user_agent = Some(user_agent.clone());
515        spark_wallet_config.leaf_auto_optimize_enabled =
516            self.config.optimization_config.auto_enabled;
517        spark_wallet_config.leaf_optimization_options.multiplicity =
518            self.config.optimization_config.multiplicity;
519        spark_wallet_config.max_concurrent_claims = self.config.max_concurrent_claims;
520
521        let shutdown_sender = watch::channel::<()>(()).0;
522
523        // Create tree store if configured
524        #[allow(unused_mut)]
525        let mut tree_store: Option<Arc<dyn TreeStore>> = self.tree_store;
526
527        #[cfg(feature = "postgres")]
528        if tree_store.is_none()
529            && let Some(ref pool) = postgres_pool
530        {
531            tree_store =
532                Some(crate::persist::postgres::create_postgres_tree_store(pool.clone()).await?);
533        }
534
535        // Create token output store if configured
536        #[allow(unused_mut)]
537        let mut token_output_store: Option<Arc<dyn TokenOutputStore>> = self.token_output_store;
538
539        #[cfg(feature = "postgres")]
540        if token_output_store.is_none()
541            && let Some(ref pool) = postgres_pool
542        {
543            token_output_store =
544                Some(crate::persist::postgres::create_postgres_token_store(pool.clone()).await?);
545        }
546
547        let mut wallet_builder =
548            spark_wallet::WalletBuilder::new(spark_wallet_config, spark_signer)
549                .with_cancellation_token(shutdown_sender.subscribe());
550        if let Some(observer) = self.payment_observer {
551            let observer: Arc<dyn spark_wallet::TransferObserver> =
552                Arc::new(SparkTransferObserver::new(observer));
553            wallet_builder = wallet_builder.with_transfer_observer(observer);
554        }
555        if let Some(tree_store) = tree_store {
556            wallet_builder = wallet_builder.with_tree_store(tree_store);
557        }
558        if let Some(token_output_store) = token_output_store {
559            wallet_builder = wallet_builder.with_token_output_store(token_output_store);
560        }
561        let spark_wallet = Arc::new(wallet_builder.build().await?);
562
563        let lnurl_server_client: Option<Arc<dyn LnurlServerClient>> = match self.lnurl_server_client
564        {
565            Some(client) => Some(client),
566            None => match &self.config.lnurl_domain {
567                Some(domain) => {
568                    let http_client: Arc<dyn platform_utils::HttpClient> =
569                        Arc::new(DefaultHttpClient::default());
570                    Some(Arc::new(DefaultLnurlServerClient::new(
571                        http_client,
572                        domain.clone(),
573                        self.config.api_key.clone(),
574                        Arc::clone(&spark_wallet),
575                    )))
576                }
577                None => None,
578            },
579        };
580
581        let event_emitter = Arc::new(EventEmitter::new(
582            self.config.real_time_sync_server_url.is_some(),
583        ));
584
585        let storage = if let Some(server_url) = &self.config.real_time_sync_server_url {
586            init_and_start_real_time_sync(RealTimeSyncParams {
587                server_url: server_url.clone(),
588                api_key: self.config.api_key.clone(),
589                user_agent,
590                signer: rtsync_signer,
591                storage: Arc::clone(&storage),
592                shutdown_receiver: shutdown_sender.subscribe(),
593                event_emitter: Arc::clone(&event_emitter),
594                lnurl_server_client: lnurl_server_client.clone(),
595            })
596            .await?
597        } else {
598            storage
599        };
600
601        // Create the MoonPay provider for buying Bitcoin
602        let buy_bitcoin_provider = Arc::new(MoonpayProvider::new(breez_server.clone()));
603
604        // Create the FlashnetTokenConverter (spawns its own refunder background task)
605        let flashnet_config = FlashnetConfig::default_config(
606            self.config.network.into(),
607            DEFAULT_INTEGRATOR_PUBKEY
608                .parse()
609                .ok()
610                .map(|pubkey| IntegratorConfig {
611                    pubkey,
612                    fee_bps: DEFAULT_INTEGRATOR_FEE_BPS,
613                }),
614        );
615        let token_converter: Arc<dyn TokenConverter> = Arc::new(FlashnetTokenConverter::new(
616            flashnet_config,
617            Arc::clone(&storage),
618            Arc::clone(&spark_wallet),
619            self.config.network,
620            shutdown_sender.subscribe(),
621        ));
622
623        // Create sync coordinator early so StableBalance can trigger syncs after conversions
624        let sync_coordinator = SyncCoordinator::new();
625
626        // Create StableBalance if configured. It spawns its own background tasks
627        // and registers itself as event middleware (must be before TokenConversionMiddleware
628        // so it can see conversion child payment events for deferred task resolution)
629        let stable_balance = if let Some(config) = &self.config.stable_balance_config {
630            Some(Arc::new(
631                StableBalance::new(
632                    config.clone(),
633                    Arc::clone(&token_converter),
634                    Arc::clone(&spark_wallet),
635                    Arc::clone(&storage),
636                    shutdown_sender.subscribe(),
637                    Arc::clone(&event_emitter),
638                    sync_coordinator.clone(),
639                )
640                .await,
641            ))
642        } else {
643            None
644        };
645
646        // Register TokenConversionMiddleware to suppress conversion child events
647        // before they reach external listeners (after StableBalance middleware)
648        event_emitter
649            .add_middleware(Box::new(TokenConversionMiddleware))
650            .await;
651
652        // Create the SDK instance
653        let sdk = BreezSdk::init_and_start(BreezSdkParams {
654            config: self.config,
655            storage,
656            chain_service,
657            fiat_service,
658            lnurl_client,
659            lnurl_server_client,
660            lnurl_auth_signer,
661            shutdown_sender,
662            spark_wallet,
663            event_emitter,
664            buy_bitcoin_provider,
665            token_converter,
666            stable_balance,
667            sync_coordinator,
668        })?;
669        debug!("Initialized and started breez sdk.");
670
671        Ok(sdk)
672    }
673}
674
675#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
676fn default_storage(
677    data_dir: &str,
678    network: Network,
679    identity_pub_key: &spark_wallet::PublicKey,
680) -> Result<Arc<dyn Storage>, SdkError> {
681    let db_path = crate::default_storage_path(data_dir, &network, identity_pub_key)?;
682    let storage = Arc::new(crate::SqliteStorage::new(&db_path)?);
683    Ok(storage)
684}