spark_config/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{collections::BTreeMap, fs::File, hash::Hash, io::Read, path::Path, str::FromStr};
4
5use anyhow::Result;
6use frost_secp256k1_tr::Identifier;
7#[cfg(feature = "with-serde")]
8use serde::{Deserialize, Serialize};
9use url::Url;
10
11/// Bitcoin network.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum BitcoinNetwork {
15    /// Mainnet.
16    Mainnet,
17
18    /// Regtest.
19    Regtest,
20
21    /// Signet.
22    Signet,
23
24    /// Testnet.
25    Testnet,
26}
27
28impl BitcoinNetwork {
29    /// Converts the BitcoinNetwork to a bitcoin::Network.
30    ///
31    /// This function is only available if the `bitcoin-conversion` feature is enabled.
32    #[cfg(feature = "bitcoin-conversion")]
33    pub fn as_bitcoin_network(&self) -> bitcoin::Network {
34        match self {
35            BitcoinNetwork::Mainnet => bitcoin::Network::Bitcoin,
36            BitcoinNetwork::Regtest => bitcoin::Network::Regtest,
37            BitcoinNetwork::Signet => bitcoin::Network::Signet,
38            BitcoinNetwork::Testnet => bitcoin::Network::Testnet,
39        }
40    }
41}
42
43/// Spark operator configuration.
44#[derive(Debug, Clone, PartialEq)]
45#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct SparkOperatorConfig {
47    id: u32,
48
49    base_url: String,
50
51    identity_public_key: String,
52
53    frost_identifier: Identifier,
54
55    running_authority: String,
56
57    #[cfg_attr(feature = "with-serde", serde(default))]
58    is_coordinator: Option<bool>,
59}
60
61impl SparkOperatorConfig {
62    /// Gets the base URL for the Spark operator.
63    pub fn base_url(&self) -> &str {
64        &self.base_url
65    }
66
67    /// Gets the identity public key for the Spark operator.
68    pub fn identity_public_key(&self) -> &str {
69        &self.identity_public_key
70    }
71
72    /// Gets the running authority for the Spark operator.
73    pub fn running_authority(&self) -> &str {
74        &self.running_authority
75    }
76
77    /// Gets the Frost identifier for the Spark operator.
78    pub fn frost_identifier(&self) -> &Identifier {
79        &self.frost_identifier
80    }
81}
82
83/// Spark Service Provider configuration.
84#[derive(Debug, Clone, PartialEq)]
85#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
86pub struct ServiceProviderConfig {
87    base_url: String,
88
89    schema_endpoint: String,
90
91    identity_public_key: String,
92
93    running_authority: String,
94}
95
96impl ServiceProviderConfig {
97    /// Gets the endpoint for the Service Provider.
98    pub fn endpoint(&self) -> Url {
99        self.force_url()
100    }
101
102    /// Gets the identity public key for the Service Provider.
103    pub fn identity_public_key(&self) -> String {
104        self.identity_public_key.clone()
105    }
106
107    /// Gets the running authority for the Service Provider.
108    pub fn running_authority(&self) -> String {
109        self.running_authority.clone()
110    }
111
112    fn force_url(&self) -> Url {
113        Url::parse(&format!("{}/{}", self.base_url, self.schema_endpoint)).unwrap()
114    }
115}
116
117/// Mempool configuration.
118#[derive(Debug, Clone, PartialEq)]
119#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
120pub struct MempoolConfig {
121    base_url: String,
122    username: String,
123    password: String,
124}
125
126impl MempoolConfig {
127    /// Gets the base URL for Mempool.
128    pub fn base_url(&self) -> &str {
129        &self.base_url
130    }
131
132    /// Gets the username for Mempool authentication.
133    pub fn username(&self) -> &str {
134        &self.username
135    }
136
137    /// Gets the password for Mempool authentication.
138    pub fn password(&self) -> &str {
139        &self.password
140    }
141}
142
143/// Electrs configuration.
144#[derive(Debug, Clone, PartialEq)]
145#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
146pub struct ElectrsConfig {
147    base_url: String,
148    username: String,
149    password: String,
150}
151
152impl ElectrsConfig {
153    /// Gets the base URL for Electrs.
154    pub fn base_url(&self) -> &str {
155        &self.base_url
156    }
157
158    /// Gets the username for Electrs authentication.
159    pub fn username(&self) -> &str {
160        &self.username
161    }
162
163    /// Gets the password for Electrs authentication.
164    pub fn password(&self) -> &str {
165        &self.password
166    }
167}
168
169/// LRC20 Node configuration.
170#[derive(Debug, Clone, PartialEq)]
171#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
172pub struct Lrc20NodeConfig {
173    base_url: String,
174}
175
176impl Lrc20NodeConfig {
177    /// Gets the base URL for the LRC20 Node.
178    pub fn base_url(&self) -> &str {
179        &self.base_url
180    }
181}
182
183/// Spark configuration.
184#[cfg_attr(
185    feature = "with-serde",
186    derive(Debug, PartialEq, Serialize, Deserialize)
187)]
188pub struct SparkConfig<K: Clone + Eq + Hash + Ord + FromStr + ToString>
189where
190    <K as FromStr>::Err: std::fmt::Debug,
191{
192    bitcoin_network: BitcoinNetwork,
193
194    operators: Vec<SparkOperatorConfig>,
195    ssp_pool: BTreeMap<K, ServiceProviderConfig>,
196
197    electrs: Option<ElectrsConfig>,
198
199    lrc20_node: Option<Lrc20NodeConfig>,
200
201    mempool: Option<MempoolConfig>,
202}
203
204impl<K: Clone + Eq + Hash + Ord + FromStr + ToString> SparkConfig<K>
205where
206    <K as FromStr>::Err: std::fmt::Debug,
207{
208    /// Create a new Spark config.
209    fn new(
210        bitcoin_network: BitcoinNetwork,
211        mempool: Option<MempoolConfig>,
212        electrs: Option<ElectrsConfig>,
213        lrc20_node: Option<Lrc20NodeConfig>,
214        ssp_pool: BTreeMap<K, ServiceProviderConfig>,
215        operators: Vec<SparkOperatorConfig>,
216    ) -> Self {
217        Self {
218            bitcoin_network,
219            mempool,
220            electrs,
221            lrc20_node,
222            ssp_pool,
223            operators,
224        }
225    }
226
227    /// Parses Spark config from TOML file.
228    #[cfg(feature = "with-serde")]
229    pub fn from_toml_file(path: &str) -> Result<Self> {
230        // Check if path is absolute
231        let config_path = if Path::new(path).is_absolute() {
232            path.to_string()
233        } else {
234            // If relative, try to use from HOME directory first
235            let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
236            format!("{}/{}", home_dir, path)
237        };
238
239        let mut file = File::open(&config_path)
240            .map_err(|_| anyhow::anyhow!("Failed to open config file at {}", config_path))?;
241
242        let mut contents = String::new();
243        file.read_to_string(&mut contents)
244            .map_err(|e| anyhow::anyhow!("Failed to read config file: {}", e))?;
245
246        #[derive(Deserialize)]
247        struct RawConfig {
248            bitcoin_network: BitcoinNetwork,
249            operators: Vec<SparkOperatorConfig>,
250            ssp_pool: BTreeMap<String, ServiceProviderConfig>,
251            mempool: Option<MempoolConfig>,
252            electrs: Option<ElectrsConfig>,
253            lrc20_node: Option<Lrc20NodeConfig>,
254        }
255
256        let raw_config: RawConfig = toml::from_str(&contents).expect("Failed to parse config");
257
258        // Validate the operators
259        for operator in &raw_config.operators {
260            if operator.identity_public_key.len() != 66 {
261                // Public keys are typically 33 bytes, represented as 66 hex characters.
262                anyhow::bail!(
263                    "Invalid identity public key length for operator {}: Expected 66 hex \
264                     characters, found {}",
265                    operator.id,
266                    operator.identity_public_key.len()
267                );
268            }
269            // TODO: Add base_url validation for operators if needed
270        }
271
272        // Parse and validate the SSP pool
273        let mut ssp_pool = BTreeMap::new();
274        for (key_str, value) in raw_config.ssp_pool {
275            // Validate URL
276            let base_url = value.base_url.trim();
277            let schema_endpoint = value.schema_endpoint.trim();
278            let full_url_str = format!("{}/{}", base_url, schema_endpoint);
279            if Url::parse(&full_url_str).is_err() {
280                panic!(
281                    "Invalid URL combination for SSP {}: {}",
282                    key_str, full_url_str
283                );
284            }
285
286            // Validate identity public key length
287            if value.identity_public_key.len() != 66 {
288                // Public keys are typically 33 bytes, represented as 66 hex characters.
289                panic!(
290                    "Invalid identity public key length for SSP {}: Expected 66 hex characters, \
291                     found {}",
292                    key_str,
293                    value.identity_public_key.len()
294                );
295            }
296
297            let key: K = key_str
298                .parse()
299                .expect(&format!("Failed to parse key: {}", key_str));
300            // Use the validated value (original value, not the trimmed copies)
301            ssp_pool.insert(key, value);
302        }
303        if ssp_pool.is_empty() {
304            tracing::warn!(
305                "No Service Provider configuration found. With the current config, you cannot use \
306                 Spark for Lightning or swaps. This can introduce functionality issues."
307            );
308        }
309
310        Ok(Self::new(
311            raw_config.bitcoin_network,
312            raw_config.mempool,
313            raw_config.electrs,
314            raw_config.lrc20_node,
315            ssp_pool,
316            raw_config.operators,
317        ))
318    }
319
320    /// Get Spark operators.
321    pub fn operators(&self) -> &Vec<SparkOperatorConfig> {
322        &self.operators
323    }
324
325    /// Get Bitcoin network.
326    pub fn bitcoin_network(&self) -> BitcoinNetwork {
327        self.bitcoin_network
328    }
329
330    /// Get Mempool base URL.
331    pub fn mempool_base_url(&self) -> Option<&str> {
332        self.mempool.as_ref().map(|config| config.base_url())
333    }
334
335    /// Get Mempool username.
336    pub fn mempool_username(&self) -> Option<&str> {
337        self.mempool.as_ref().map(|config| config.username())
338    }
339
340    /// Get Mempool password.
341    pub fn mempool_password(&self) -> Option<&str> {
342        self.mempool.as_ref().map(|config| config.password())
343    }
344
345    /// Get Mempool authentication.
346    pub fn mempool_auth(&self) -> Option<(&str, &str)> {
347        if let Some(config) = &self.mempool {
348            Some((config.username(), config.password()))
349        } else {
350            None
351        }
352    }
353
354    /// Get Electrs base URL.
355    pub fn electrs_base_url(&self) -> Option<&str> {
356        self.electrs.as_ref().map(|config| config.base_url())
357    }
358
359    /// Get Electrs authentication.
360    pub fn electrs_auth(&self) -> Option<(&str, &str)> {
361        if let Some(config) = &self.electrs {
362            Some((config.username(), config.password()))
363        } else {
364            None
365        }
366    }
367
368    /// Get Bitcoin base URL (alias for electrs_base_url).
369    pub fn bitcoin_base_url(&self) -> Option<&str> {
370        self.electrs_base_url()
371    }
372
373    /// Get LRC20 Node base URL.
374    pub fn lrc20_node_url(&self) -> Option<&str> {
375        self.lrc20_node.as_ref().map(|config| config.base_url())
376    }
377
378    /// Returns the endpoint for the SSP for the given key.
379    pub fn ssp_endpoint(&self, key: &K) -> Option<Url> {
380        self.ssp_pool.get(key).map(|ssp| ssp.endpoint())
381    }
382
383    /// Returns the coordinator operator config, if one is designated.
384    pub fn coordinator_operator(&self) -> Option<&SparkOperatorConfig> {
385        self.operators
386            .iter()
387            .find(|op| op.is_coordinator == Some(true))
388    }
389}