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, de::DeserializeOwned};
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)]
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)]
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)]
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)]
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)]
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#[cfg(not(feature = "with-serde"))]
184pub trait DeserializeOwned {}
185#[cfg(not(feature = "with-serde"))]
186impl<T: Clone> DeserializeOwned for T {}
187
188#[cfg(not(feature = "with-serde"))]
189pub trait Serialize {}
190#[cfg(not(feature = "with-serde"))]
191impl<T: Clone> Serialize for T {}
192
193/// Spark configuration.
194pub struct SparkConfig<K: Eq + Hash + Ord + FromStr + ToString + DeserializeOwned + Serialize>
195where
196    <K as FromStr>::Err: std::fmt::Debug,
197{
198    bitcoin_network: BitcoinNetwork,
199
200    operators: Vec<SparkOperatorConfig>,
201    ssp_pool: BTreeMap<K, ServiceProviderConfig>,
202
203    electrs: Option<ElectrsConfig>,
204
205    lrc20_node: Option<Lrc20NodeConfig>,
206
207    mempool: Option<MempoolConfig>,
208}
209
210impl<K: Eq + Hash + Ord + FromStr + ToString + DeserializeOwned + Serialize> SparkConfig<K>
211where
212    <K as FromStr>::Err: std::fmt::Debug,
213{
214    /// Create a new Spark config.
215    fn new(
216        bitcoin_network: BitcoinNetwork,
217        mempool: Option<MempoolConfig>,
218        electrs: Option<ElectrsConfig>,
219        lrc20_node: Option<Lrc20NodeConfig>,
220        ssp_pool: BTreeMap<K, ServiceProviderConfig>,
221        operators: Vec<SparkOperatorConfig>,
222    ) -> Self {
223        Self {
224            bitcoin_network,
225            mempool,
226            electrs,
227            lrc20_node,
228            ssp_pool,
229            operators,
230        }
231    }
232
233    /// Parses Spark config from TOML file.
234    #[cfg(feature = "with-serde")]
235    pub fn from_toml_file(path: &str) -> Result<Self> {
236        // Check if path is absolute
237        let config_path = if Path::new(path).is_absolute() {
238            path.to_string()
239        } else {
240            // If relative, try to use from HOME directory first
241            let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
242            format!("{}/{}", home_dir, path)
243        };
244
245        let mut file = File::open(&config_path)
246            .map_err(|_| anyhow::anyhow!("Failed to open config file at {}", config_path))?;
247
248        let mut contents = String::new();
249        file.read_to_string(&mut contents)
250            .map_err(|e| anyhow::anyhow!("Failed to read config file: {}", e))?;
251
252        #[derive(Deserialize)]
253        struct RawConfig {
254            bitcoin_network: BitcoinNetwork,
255            operators: Vec<SparkOperatorConfig>,
256            ssp_pool: BTreeMap<String, ServiceProviderConfig>,
257            mempool: Option<MempoolConfig>,
258            electrs: Option<ElectrsConfig>,
259            lrc20_node: Option<Lrc20NodeConfig>,
260        }
261
262        let raw_config: RawConfig = toml::from_str(&contents).expect("Failed to parse config");
263
264        // Validate the operators
265        for operator in &raw_config.operators {
266            if operator.identity_public_key.len() != 66 {
267                // Public keys are typically 33 bytes, represented as 66 hex characters.
268                anyhow::bail!(
269                    "Invalid identity public key length for operator {}: Expected 66 hex \
270                     characters, found {}",
271                    operator.id,
272                    operator.identity_public_key.len()
273                );
274            }
275            // TODO: Add base_url validation for operators if needed
276        }
277
278        // Parse and validate the SSP pool
279        let mut ssp_pool = BTreeMap::new();
280        for (key_str, value) in raw_config.ssp_pool {
281            // Validate URL
282            let base_url = value.base_url.trim();
283            let schema_endpoint = value.schema_endpoint.trim();
284            let full_url_str = format!("{}/{}", base_url, schema_endpoint);
285            if Url::parse(&full_url_str).is_err() {
286                panic!(
287                    "Invalid URL combination for SSP {}: {}",
288                    key_str, full_url_str
289                );
290            }
291
292            // Validate identity public key length
293            if value.identity_public_key.len() != 66 {
294                // Public keys are typically 33 bytes, represented as 66 hex characters.
295                panic!(
296                    "Invalid identity public key length for SSP {}: Expected 66 hex characters, \
297                     found {}",
298                    key_str,
299                    value.identity_public_key.len()
300                );
301            }
302
303            let key: K = key_str
304                .parse()
305                .expect(&format!("Failed to parse key: {}", key_str));
306            // Use the validated value (original value, not the trimmed copies)
307            ssp_pool.insert(key, value);
308        }
309        if ssp_pool.is_empty() {
310            tracing::warn!(
311                "No Service Provider configuration found. With the current config, you cannot use \
312                 Spark for Lightning or swaps. This can introduce functionality issues."
313            );
314        }
315
316        Ok(Self::new(
317            raw_config.bitcoin_network,
318            raw_config.mempool,
319            raw_config.electrs,
320            raw_config.lrc20_node,
321            ssp_pool,
322            raw_config.operators,
323        ))
324    }
325
326    /// Get Spark operators.
327    pub fn operators(&self) -> &Vec<SparkOperatorConfig> {
328        &self.operators
329    }
330
331    /// Get Bitcoin network.
332    pub fn bitcoin_network(&self) -> BitcoinNetwork {
333        self.bitcoin_network
334    }
335
336    /// Get Mempool base URL.
337    pub fn mempool_base_url(&self) -> Option<&str> {
338        self.mempool.as_ref().map(|config| config.base_url())
339    }
340
341    /// Get Mempool username.
342    pub fn mempool_username(&self) -> Option<&str> {
343        self.mempool.as_ref().map(|config| config.username())
344    }
345
346    /// Get Mempool password.
347    pub fn mempool_password(&self) -> Option<&str> {
348        self.mempool.as_ref().map(|config| config.password())
349    }
350
351    /// Get Mempool authentication.
352    pub fn mempool_auth(&self) -> Option<(&str, &str)> {
353        if let Some(config) = &self.mempool {
354            Some((config.username(), config.password()))
355        } else {
356            None
357        }
358    }
359
360    /// Get Electrs base URL.
361    pub fn electrs_base_url(&self) -> Option<&str> {
362        self.electrs.as_ref().map(|config| config.base_url())
363    }
364
365    /// Get Electrs authentication.
366    pub fn electrs_auth(&self) -> Option<(&str, &str)> {
367        if let Some(config) = &self.electrs {
368            Some((config.username(), config.password()))
369        } else {
370            None
371        }
372    }
373
374    /// Get Bitcoin base URL (alias for electrs_base_url).
375    pub fn bitcoin_base_url(&self) -> Option<&str> {
376        self.electrs_base_url()
377    }
378
379    /// Get LRC20 Node base URL.
380    pub fn lrc20_node_url(&self) -> Option<&str> {
381        self.lrc20_node.as_ref().map(|config| config.base_url())
382    }
383
384    /// Returns the endpoint for the SSP for the given key.
385    pub fn ssp_endpoint(&self, key: &K) -> Option<Url> {
386        self.ssp_pool.get(key).map(|ssp| ssp.endpoint())
387    }
388
389    /// Returns the coordinator operator config, if one is designated.
390    pub fn coordinator_operator(&self) -> Option<&SparkOperatorConfig> {
391        self.operators
392            .iter()
393            .find(|op| op.is_coordinator == Some(true))
394    }
395}