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#[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)]
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)]
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)]
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)]
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)]
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(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
193pub 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 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 #[cfg(feature = "with-serde")]
235 pub fn from_toml_file(path: &str) -> Result<Self> {
236 let config_path = if Path::new(path).is_absolute() {
238 path.to_string()
239 } else {
240 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 for operator in &raw_config.operators {
266 if operator.identity_public_key.len() != 66 {
267 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 }
277
278 let mut ssp_pool = BTreeMap::new();
280 for (key_str, value) in raw_config.ssp_pool {
281 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 if value.identity_public_key.len() != 66 {
294 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 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 pub fn operators(&self) -> &Vec<SparkOperatorConfig> {
328 &self.operators
329 }
330
331 pub fn bitcoin_network(&self) -> BitcoinNetwork {
333 self.bitcoin_network
334 }
335
336 pub fn mempool_base_url(&self) -> Option<&str> {
338 self.mempool.as_ref().map(|config| config.base_url())
339 }
340
341 pub fn mempool_username(&self) -> Option<&str> {
343 self.mempool.as_ref().map(|config| config.username())
344 }
345
346 pub fn mempool_password(&self) -> Option<&str> {
348 self.mempool.as_ref().map(|config| config.password())
349 }
350
351 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 pub fn electrs_base_url(&self) -> Option<&str> {
362 self.electrs.as_ref().map(|config| config.base_url())
363 }
364
365 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 pub fn bitcoin_base_url(&self) -> Option<&str> {
376 self.electrs_base_url()
377 }
378
379 pub fn lrc20_node_url(&self) -> Option<&str> {
381 self.lrc20_node.as_ref().map(|config| config.base_url())
382 }
383
384 pub fn ssp_endpoint(&self, key: &K) -> Option<Url> {
386 self.ssp_pool.get(key).map(|ssp| ssp.endpoint())
387 }
388
389 pub fn coordinator_operator(&self) -> Option<&SparkOperatorConfig> {
391 self.operators
392 .iter()
393 .find(|op| op.is_coordinator == Some(true))
394 }
395}