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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum BitcoinNetwork {
15 Mainnet,
17
18 Regtest,
20
21 Signet,
23
24 Testnet,
26}
27
28impl BitcoinNetwork {
29 #[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#[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 pub fn base_url(&self) -> &str {
64 &self.base_url
65 }
66
67 pub fn identity_public_key(&self) -> &str {
69 &self.identity_public_key
70 }
71
72 pub fn running_authority(&self) -> &str {
74 &self.running_authority
75 }
76
77 pub fn frost_identifier(&self) -> &Identifier {
79 &self.frost_identifier
80 }
81}
82
83#[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 pub fn endpoint(&self) -> Url {
99 self.force_url()
100 }
101
102 pub fn identity_public_key(&self) -> String {
104 self.identity_public_key.clone()
105 }
106
107 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#[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 pub fn base_url(&self) -> &str {
129 &self.base_url
130 }
131
132 pub fn username(&self) -> &str {
134 &self.username
135 }
136
137 pub fn password(&self) -> &str {
139 &self.password
140 }
141}
142
143#[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 pub fn base_url(&self) -> &str {
155 &self.base_url
156 }
157
158 pub fn username(&self) -> &str {
160 &self.username
161 }
162
163 pub fn password(&self) -> &str {
165 &self.password
166 }
167}
168
169#[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 pub fn base_url(&self) -> &str {
179 &self.base_url
180 }
181}
182
183#[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 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 #[cfg(feature = "with-serde")]
229 pub fn from_toml_file(path: &str) -> Result<Self> {
230 let config_path = if Path::new(path).is_absolute() {
232 path.to_string()
233 } else {
234 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 for operator in &raw_config.operators {
260 if operator.identity_public_key.len() != 66 {
261 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 }
271
272 let mut ssp_pool = BTreeMap::new();
274 for (key_str, value) in raw_config.ssp_pool {
275 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 if value.identity_public_key.len() != 66 {
288 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 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 pub fn operators(&self) -> &Vec<SparkOperatorConfig> {
322 &self.operators
323 }
324
325 pub fn bitcoin_network(&self) -> BitcoinNetwork {
327 self.bitcoin_network
328 }
329
330 pub fn mempool_base_url(&self) -> Option<&str> {
332 self.mempool.as_ref().map(|config| config.base_url())
333 }
334
335 pub fn mempool_username(&self) -> Option<&str> {
337 self.mempool.as_ref().map(|config| config.username())
338 }
339
340 pub fn mempool_password(&self) -> Option<&str> {
342 self.mempool.as_ref().map(|config| config.password())
343 }
344
345 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 pub fn electrs_base_url(&self) -> Option<&str> {
356 self.electrs.as_ref().map(|config| config.base_url())
357 }
358
359 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 pub fn bitcoin_base_url(&self) -> Option<&str> {
370 self.electrs_base_url()
371 }
372
373 pub fn lrc20_node_url(&self) -> Option<&str> {
375 self.lrc20_node.as_ref().map(|config| config.base_url())
376 }
377
378 pub fn ssp_endpoint(&self, key: &K) -> Option<Url> {
380 self.ssp_pool.get(key).map(|ssp| ssp.endpoint())
381 }
382
383 pub fn coordinator_operator(&self) -> Option<&SparkOperatorConfig> {
385 self.operators
386 .iter()
387 .find(|op| op.is_coordinator == Some(true))
388 }
389}