1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::{Debug, Display};
3use std::hash::Hash;
4use std::path::Path;
5use std::str::FromStr;
6
7use anyhow::{Context, format_err};
8use bitcoin::hashes::sha256::HashEngine;
9use bitcoin::hashes::{Hash as BitcoinHash, hex, sha256};
10use bls12_381::Scalar;
11use fedimint_core::core::{ModuleInstanceId, ModuleKind};
12use fedimint_core::encoding::{DynRawFallback, Encodable};
13use fedimint_core::module::registry::ModuleRegistry;
14use fedimint_core::util::SafeUrl;
15use fedimint_core::{ModuleDecoderRegistry, format_hex};
16use fedimint_logging::LOG_CORE;
17use hex::FromHex;
18use secp256k1::PublicKey;
19use serde::de::DeserializeOwned;
20use serde::{Deserialize, Deserializer, Serialize, Serializer};
21use serde_json::json;
22use threshold_crypto::{G1Projective, G2Projective};
23use tracing::warn;
24
25use crate::core::DynClientConfig;
26use crate::encoding::Decodable;
27use crate::module::{
28 CoreConsensusVersion, DynCommonModuleInit, IDynCommonModuleInit, ModuleConsensusVersion,
29 SerdeModuleEncoding,
30};
31use crate::session_outcome::SignedSessionOutcome;
32use crate::{PeerId, maybe_add_send_sync, secp256k1};
33
34pub const ALEPH_BFT_UNIT_BYTE_LIMIT: usize = 50_000;
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct JsonWithKind {
45 kind: ModuleKind,
46 #[serde(flatten)]
47 value: serde_json::Value,
48}
49
50impl JsonWithKind {
51 pub fn new(kind: ModuleKind, value: serde_json::Value) -> Self {
52 Self { kind, value }
53 }
54
55 pub fn with_fixed_empty_value(self) -> Self {
76 if let serde_json::Value::Object(ref o) = self.value
77 && o.is_empty()
78 {
79 return Self {
80 kind: self.kind,
81 value: serde_json::Value::Null,
82 };
83 }
84
85 self
86 }
87
88 pub fn value(&self) -> &serde_json::Value {
89 &self.value
90 }
91
92 pub fn kind(&self) -> &ModuleKind {
93 &self.kind
94 }
95
96 pub fn is_kind(&self, kind: &ModuleKind) -> bool {
97 &self.kind == kind
98 }
99}
100
101#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
102pub struct PeerUrl {
103 pub url: SafeUrl,
105 pub name: String,
107}
108
109#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
113pub struct ClientConfigV0 {
114 #[serde(flatten)]
115 pub global: GlobalClientConfigV0,
116 #[serde(deserialize_with = "de_int_key")]
117 pub modules: BTreeMap<ModuleInstanceId, ClientModuleConfig>,
118}
119
120#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
124pub struct ClientConfig {
125 #[serde(flatten)]
126 pub global: GlobalClientConfig,
127 #[serde(deserialize_with = "de_int_key")]
128 pub modules: BTreeMap<ModuleInstanceId, ClientModuleConfig>,
129}
130
131fn de_int_key<'de, D, K, V>(deserializer: D) -> Result<BTreeMap<K, V>, D::Error>
133where
134 D: Deserializer<'de>,
135 K: Eq + Ord + FromStr,
136 K::Err: Display,
137 V: Deserialize<'de>,
138{
139 let string_map = <BTreeMap<String, V>>::deserialize(deserializer)?;
140 let map = string_map
141 .into_iter()
142 .map(|(key_str, value)| {
143 let key = K::from_str(&key_str).map_err(serde::de::Error::custom)?;
144 Ok((key, value))
145 })
146 .collect::<Result<BTreeMap<_, _>, _>>()?;
147 Ok(map)
148}
149
150fn optional_de_int_key<'de, D, K, V>(deserializer: D) -> Result<Option<BTreeMap<K, V>>, D::Error>
151where
152 D: Deserializer<'de>,
153 K: Eq + Ord + FromStr,
154 K::Err: Display,
155 V: Deserialize<'de>,
156{
157 let Some(string_map) = <Option<BTreeMap<String, V>>>::deserialize(deserializer)? else {
158 return Ok(None);
159 };
160
161 let map = string_map
162 .into_iter()
163 .map(|(key_str, value)| {
164 let key = K::from_str(&key_str).map_err(serde::de::Error::custom)?;
165 Ok((key, value))
166 })
167 .collect::<Result<BTreeMap<_, _>, _>>()?;
168
169 Ok(Some(map))
170}
171
172#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
175pub struct JsonClientConfig {
176 pub global: GlobalClientConfig,
177 pub modules: BTreeMap<ModuleInstanceId, JsonWithKind>,
178}
179
180#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
182pub struct GlobalClientConfigV0 {
183 #[serde(deserialize_with = "de_int_key")]
185 pub api_endpoints: BTreeMap<PeerId, PeerUrl>,
186 pub consensus_version: CoreConsensusVersion,
188 pub meta: BTreeMap<String, String>,
191}
192
193#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
195pub struct GlobalClientConfig {
196 #[serde(deserialize_with = "de_int_key")]
198 pub api_endpoints: BTreeMap<PeerId, PeerUrl>,
199 #[serde(default, deserialize_with = "optional_de_int_key")]
202 pub broadcast_public_keys: Option<BTreeMap<PeerId, PublicKey>>,
203 pub consensus_version: CoreConsensusVersion,
205 pub meta: BTreeMap<String, String>,
208}
209
210impl GlobalClientConfig {
211 pub fn calculate_federation_id(&self) -> FederationId {
214 FederationId(self.api_endpoints.consensus_hash())
215 }
216
217 pub fn federation_name(&self) -> Option<&str> {
219 self.meta.get(META_FEDERATION_NAME_KEY).map(|x| &**x)
220 }
221}
222
223impl ClientConfig {
224 pub fn redecode_raw(
226 self,
227 modules: &ModuleDecoderRegistry,
228 ) -> Result<Self, crate::encoding::DecodeError> {
229 Ok(Self {
230 modules: self
231 .modules
232 .into_iter()
233 .map(|(module_id, v)| {
234 let kind = v.kind.clone();
237
238 v.redecode_raw(modules)
239 .context(format!("redecode_raw: instance: {module_id}, kind: {kind}"))
240 .map(|v| (module_id, v))
241 })
242 .collect::<Result<_, _>>()?,
243 ..self
244 })
245 }
246
247 pub fn calculate_federation_id(&self) -> FederationId {
248 self.global.calculate_federation_id()
249 }
250
251 pub fn meta<V: serde::de::DeserializeOwned + 'static>(
253 &self,
254 key: &str,
255 ) -> Result<Option<V>, anyhow::Error> {
256 let Some(str_value) = self.global.meta.get(key) else {
257 return Ok(None);
258 };
259 let res = serde_json::from_str(str_value)
260 .map(Some)
261 .context(format!("Decoding meta field '{key}' failed"));
262
263 if res.is_err() && std::any::TypeId::of::<V>() == std::any::TypeId::of::<String>() {
267 let string_ret = Box::new(str_value.clone());
268 let ret = unsafe {
269 std::mem::transmute::<Box<String>, Box<V>>(string_ret)
271 };
272 Ok(Some(*ret))
273 } else {
274 res
275 }
276 }
277
278 pub fn to_json(&self) -> JsonClientConfig {
284 JsonClientConfig {
285 global: self.global.clone(),
286 modules: self
287 .modules
288 .iter()
289 .map(|(&module_instance_id, module_config)| {
290 let module_config_json = JsonWithKind {
291 kind: module_config.kind.clone(),
292 value: module_config.config
293 .clone()
294 .decoded()
295 .and_then(|dyn_cfg| dyn_cfg.to_json())
296 .unwrap_or_else(|| json!({
297 "unknown_module_hex": module_config.config.consensus_encode_to_hex()
298 })),
299 };
300 (module_instance_id, module_config_json)
301 })
302 .collect(),
303 }
304 }
305}
306
307#[derive(
313 Debug,
314 Copy,
315 Serialize,
316 Deserialize,
317 Clone,
318 Eq,
319 Hash,
320 PartialEq,
321 Encodable,
322 Decodable,
323 Ord,
324 PartialOrd,
325)]
326pub struct FederationId(pub sha256::Hash);
327
328#[derive(
329 Debug,
330 Copy,
331 Serialize,
332 Deserialize,
333 Clone,
334 Eq,
335 Hash,
336 PartialEq,
337 Encodable,
338 Decodable,
339 Ord,
340 PartialOrd,
341)]
342pub struct FederationIdPrefix([u8; 4]);
348
349impl Display for FederationIdPrefix {
350 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
351 format_hex(&self.0, f)
352 }
353}
354
355impl Display for FederationId {
356 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
357 format_hex(&self.0.to_byte_array(), f)
358 }
359}
360
361impl FromStr for FederationIdPrefix {
362 type Err = anyhow::Error;
363
364 fn from_str(s: &str) -> Result<Self, Self::Err> {
365 Ok(Self(<[u8; 4]>::from_hex(s)?))
366 }
367}
368
369impl FederationIdPrefix {
370 pub fn to_bytes(&self) -> Vec<u8> {
371 self.0.to_vec()
372 }
373}
374
375impl FederationId {
377 pub fn dummy() -> Self {
379 Self(sha256::Hash::from_byte_array([42; 32]))
380 }
381
382 pub(crate) fn from_byte_array(bytes: [u8; 32]) -> Self {
383 Self(sha256::Hash::from_byte_array(bytes))
384 }
385
386 pub fn to_prefix(&self) -> FederationIdPrefix {
387 FederationIdPrefix(self.0[..4].try_into().expect("can't fail"))
388 }
389
390 pub fn to_fake_ln_pub_key(
400 &self,
401 secp: &bitcoin::secp256k1::Secp256k1<bitcoin::secp256k1::All>,
402 ) -> anyhow::Result<bitcoin::secp256k1::PublicKey> {
403 let sk = bitcoin::secp256k1::SecretKey::from_slice(&self.0.to_byte_array())?;
404 Ok(bitcoin::secp256k1::PublicKey::from_secret_key(secp, &sk))
405 }
406}
407
408impl FromStr for FederationId {
409 type Err = anyhow::Error;
410
411 fn from_str(s: &str) -> Result<Self, Self::Err> {
412 Ok(Self::from_byte_array(<[u8; 32]>::from_hex(s)?))
413 }
414}
415
416impl ClientConfig {
417 pub fn consensus_hash(&self) -> sha256::Hash {
419 let mut engine = HashEngine::default();
420 self.consensus_encode(&mut engine)
421 .expect("Consensus hashing should never fail");
422 sha256::Hash::from_engine(engine)
423 }
424
425 pub fn get_module<T: Decodable + 'static>(&self, id: ModuleInstanceId) -> anyhow::Result<&T> {
426 self.modules.get(&id).map_or_else(
427 || Err(format_err!("Client config for module id {id} not found")),
428 |client_cfg| client_cfg.cast(),
429 )
430 }
431
432 pub fn get_module_cfg(&self, id: ModuleInstanceId) -> anyhow::Result<ClientModuleConfig> {
434 self.modules.get(&id).map_or_else(
435 || Err(format_err!("Client config for module id {id} not found")),
436 |client_cfg| Ok(client_cfg.clone()),
437 )
438 }
439
440 pub fn get_first_module_by_kind<T: Decodable + 'static>(
448 &self,
449 kind: impl Into<ModuleKind>,
450 ) -> anyhow::Result<(ModuleInstanceId, &T)> {
451 let kind: ModuleKind = kind.into();
452 let Some((id, module_cfg)) = self.modules.iter().find(|(_, v)| v.is_kind(&kind)) else {
453 anyhow::bail!("Module kind {kind} not found")
454 };
455 Ok((*id, module_cfg.cast()?))
456 }
457
458 pub fn get_first_module_by_kind_cfg(
460 &self,
461 kind: impl Into<ModuleKind>,
462 ) -> anyhow::Result<(ModuleInstanceId, ClientModuleConfig)> {
463 let kind: ModuleKind = kind.into();
464 self.modules
465 .iter()
466 .find(|(_, v)| v.is_kind(&kind))
467 .map(|(id, v)| (*id, v.clone()))
468 .ok_or_else(|| anyhow::format_err!("Module kind {kind} not found"))
469 }
470}
471
472#[derive(Clone, Debug)]
473pub struct ModuleInitRegistry<M>(BTreeMap<ModuleKind, M>);
474
475const LEGACY_MODULE_ORDER: &[&str] = &["ln", "mint", "wallet", "lnv2", "meta", "unknown"];
478
479impl<M> ModuleInitRegistry<M> {
480 pub fn iter(&self) -> impl Iterator<Item = (&ModuleKind, &M)> {
481 self.0.iter()
482 }
483
484 pub fn iter_legacy_order(&self) -> Vec<(&ModuleKind, &M)> {
488 let mut ordered: Vec<(&ModuleKind, &M)> = Vec::new();
489
490 for kind_str in LEGACY_MODULE_ORDER {
492 let kind = ModuleKind::from_static_str(kind_str);
493 if let Some((k, m)) = self.0.get_key_value(&kind) {
494 ordered.push((k, m));
495 }
496 }
497
498 for (kind, module) in &self.0 {
500 if !LEGACY_MODULE_ORDER.contains(&kind.as_str()) {
501 ordered.push((kind, module));
502 }
503 }
504
505 ordered
506 }
507}
508
509impl<M> Default for ModuleInitRegistry<M> {
510 fn default() -> Self {
511 Self(BTreeMap::new())
512 }
513}
514
515pub type CommonModuleInitRegistry = ModuleInitRegistry<DynCommonModuleInit>;
516
517impl<M> From<Vec<M>> for ModuleInitRegistry<M>
518where
519 M: AsRef<dyn IDynCommonModuleInit + Send + Sync + 'static>,
520{
521 fn from(value: Vec<M>) -> Self {
522 Self(
523 value
524 .into_iter()
525 .map(|i| (i.as_ref().module_kind(), i))
526 .collect::<BTreeMap<_, _>>(),
527 )
528 }
529}
530
531impl<M> FromIterator<M> for ModuleInitRegistry<M>
532where
533 M: AsRef<maybe_add_send_sync!(dyn IDynCommonModuleInit + 'static)>,
534{
535 fn from_iter<T: IntoIterator<Item = M>>(iter: T) -> Self {
536 Self(
537 iter.into_iter()
538 .map(|i| (i.as_ref().module_kind(), i))
539 .collect::<BTreeMap<_, _>>(),
540 )
541 }
542}
543
544impl<M> ModuleInitRegistry<M> {
545 pub fn new() -> Self {
546 Self::default()
547 }
548
549 pub fn attach<T>(&mut self, r#gen: T)
550 where
551 T: Into<M> + 'static + Send + Sync,
552 M: AsRef<dyn IDynCommonModuleInit + 'static + Send + Sync>,
553 {
554 let r#gen: M = r#gen.into();
555 let kind = r#gen.as_ref().module_kind();
556 assert!(
557 self.0.insert(kind.clone(), r#gen).is_none(),
558 "Can't insert module of same kind twice: {kind}"
559 );
560 }
561
562 pub fn kinds(&self) -> BTreeSet<ModuleKind> {
563 self.0.keys().cloned().collect()
564 }
565
566 pub fn get(&self, k: &ModuleKind) -> Option<&M> {
567 self.0.get(k)
568 }
569}
570
571impl<M> ModuleInitRegistry<M>
572where
573 M: AsRef<dyn IDynCommonModuleInit + Send + Sync + 'static>,
574{
575 #[deprecated(
576 note = "You probably want `available_decoders` to support missing module kinds. If you really want a strict behavior, use `decoders_strict`"
577 )]
578 pub fn decoders<'a>(
579 &self,
580 modules: impl Iterator<Item = (ModuleInstanceId, &'a ModuleKind)>,
581 ) -> anyhow::Result<ModuleDecoderRegistry> {
582 self.decoders_strict(modules)
583 }
584
585 pub fn decoders_strict<'a>(
587 &self,
588 modules: impl Iterator<Item = (ModuleInstanceId, &'a ModuleKind)>,
589 ) -> anyhow::Result<ModuleDecoderRegistry> {
590 let mut decoders = BTreeMap::new();
591 for (id, kind) in modules {
592 let Some(init) = self.0.get(kind) else {
593 anyhow::bail!(
594 "Detected configuration for unsupported module id: {id}, kind: {kind}"
595 )
596 };
597
598 decoders.insert(id, (kind.clone(), init.as_ref().decoder()));
599 }
600 Ok(ModuleDecoderRegistry::from(decoders))
601 }
602
603 pub fn available_decoders<'a>(
605 &self,
606 modules: impl Iterator<Item = (ModuleInstanceId, &'a ModuleKind)>,
607 ) -> anyhow::Result<ModuleDecoderRegistry> {
608 let mut decoders = BTreeMap::new();
609 for (id, kind) in modules {
610 let Some(init) = self.0.get(kind) else {
611 warn!(target: LOG_CORE, "Unsupported module id: {id}, kind: {kind}");
612 continue;
613 };
614
615 decoders.insert(id, (kind.clone(), init.as_ref().decoder()));
616 }
617 Ok(ModuleDecoderRegistry::from(decoders))
618 }
619}
620
621#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
622pub struct ServerModuleConsensusConfig {
623 pub kind: ModuleKind,
624 pub version: ModuleConsensusVersion,
625 #[serde(with = "::hex::serde")]
626 pub config: Vec<u8>,
627}
628
629#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
635pub struct ClientModuleConfig {
636 pub kind: ModuleKind,
637 pub version: ModuleConsensusVersion,
638 #[serde(with = "::fedimint_core::encoding::as_hex")]
639 pub config: DynRawFallback<DynClientConfig>,
640}
641
642impl ClientModuleConfig {
643 pub fn from_typed<T: fedimint_core::core::ClientConfig>(
644 module_instance_id: ModuleInstanceId,
645 kind: ModuleKind,
646 version: ModuleConsensusVersion,
647 value: T,
648 ) -> anyhow::Result<Self> {
649 Ok(Self {
650 kind,
651 version,
652 config: fedimint_core::core::DynClientConfig::from_typed(module_instance_id, value)
653 .into(),
654 })
655 }
656
657 pub fn redecode_raw(
658 self,
659 modules: &ModuleDecoderRegistry,
660 ) -> Result<Self, crate::encoding::DecodeError> {
661 Ok(Self {
662 config: self.config.redecode_raw(modules)?,
663 ..self
664 })
665 }
666
667 pub fn is_kind(&self, kind: &ModuleKind) -> bool {
668 &self.kind == kind
669 }
670
671 pub fn kind(&self) -> &ModuleKind {
672 &self.kind
673 }
674}
675
676impl ClientModuleConfig {
677 pub fn cast<T>(&self) -> anyhow::Result<&T>
678 where
679 T: 'static,
680 {
681 self.config
682 .expect_decoded_ref()
683 .as_any()
684 .downcast_ref::<T>()
685 .context("can't convert client module config to desired type")
686 }
687}
688
689#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
693pub struct ServerModuleConfig {
694 pub private: JsonWithKind,
695 pub consensus: ServerModuleConsensusConfig,
696}
697
698impl ServerModuleConfig {
699 pub fn from(private: JsonWithKind, consensus: ServerModuleConsensusConfig) -> Self {
700 Self { private, consensus }
701 }
702
703 pub fn to_typed<T: TypedServerModuleConfig>(&self) -> anyhow::Result<T> {
704 let private = serde_json::from_value(self.private.value().clone())?;
705 let consensus = <T::Consensus>::consensus_decode_whole(
706 &self.consensus.config[..],
707 &ModuleRegistry::default(),
708 )?;
709
710 Ok(TypedServerModuleConfig::from_parts(private, consensus))
711 }
712}
713
714pub trait TypedServerModuleConsensusConfig:
716 DeserializeOwned + Serialize + Encodable + Decodable
717{
718 fn kind(&self) -> ModuleKind;
719
720 fn version(&self) -> ModuleConsensusVersion;
721
722 fn from_erased(erased: &ServerModuleConsensusConfig) -> anyhow::Result<Self> {
723 Ok(Self::consensus_decode_whole(
724 &erased.config[..],
725 &ModuleRegistry::default(),
726 )?)
727 }
728}
729
730pub trait TypedServerModuleConfig: DeserializeOwned + Serialize {
732 type Private: DeserializeOwned + Serialize;
735 type Consensus: TypedServerModuleConsensusConfig;
737
738 fn from_parts(private: Self::Private, consensus: Self::Consensus) -> Self;
740
741 fn to_parts(self) -> (ModuleKind, Self::Private, Self::Consensus);
743
744 fn to_erased(self) -> ServerModuleConfig {
746 let (kind, private, consensus) = self.to_parts();
747
748 ServerModuleConfig {
749 private: JsonWithKind::new(
750 kind,
751 serde_json::to_value(private).expect("serialization can't fail"),
752 ),
753 consensus: ServerModuleConsensusConfig {
754 kind: consensus.kind(),
755 version: consensus.version(),
756 config: consensus.consensus_encode_to_vec(),
757 },
758 }
759 }
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encodable, Decodable)]
763pub enum P2PMessage {
764 Aleph(Vec<u8>),
765 SessionSignature(secp256k1::schnorr::Signature),
766 SessionIndex(u64),
767 SignedSessionOutcome(SerdeModuleEncoding<SignedSessionOutcome>),
768 Checksum(sha256::Hash),
769 DkgG1(DkgMessageG1),
770 DkgG2(DkgMessageG2),
771 Encodable(Vec<u8>),
772 #[encodable_default]
773 Default {
774 variant: u64,
775 bytes: Vec<u8>,
776 },
777}
778
779#[derive(Debug, PartialEq, Eq, Clone, Encodable, Decodable)]
780pub enum DkgMessageG1 {
781 Hash(sha256::Hash),
782 Commitment(Vec<G1Projective>),
783 Share(Scalar),
784}
785
786#[derive(Debug, PartialEq, Eq, Clone, Encodable, Decodable)]
787pub enum DkgMessageG2 {
788 Hash(sha256::Hash),
789 Commitment(Vec<G2Projective>),
790 Share(Scalar),
791}
792
793impl Serialize for DkgMessageG1 {
796 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
797 where
798 S: Serializer,
799 {
800 self.consensus_encode_to_hex().serialize(serializer)
801 }
802}
803
804impl<'de> Deserialize<'de> for DkgMessageG1 {
805 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
806 where
807 D: Deserializer<'de>,
808 {
809 Self::consensus_decode_hex(
810 &String::deserialize(deserializer)?,
811 &ModuleDecoderRegistry::default(),
812 )
813 .map_err(serde::de::Error::custom)
814 }
815}
816
817impl Serialize for DkgMessageG2 {
820 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
821 where
822 S: Serializer,
823 {
824 self.consensus_encode_to_hex().serialize(serializer)
825 }
826}
827
828impl<'de> Deserialize<'de> for DkgMessageG2 {
829 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
830 where
831 D: Deserializer<'de>,
832 {
833 Self::consensus_decode_hex(
834 &String::deserialize(deserializer)?,
835 &ModuleDecoderRegistry::default(),
836 )
837 .map_err(serde::de::Error::custom)
838 }
839}
840
841pub const META_FEDERATION_NAME_KEY: &str = "federation_name";
844
845pub fn load_from_file<T: DeserializeOwned>(path: &Path) -> Result<T, anyhow::Error> {
846 let file = std::fs::File::open(path)?;
847 Ok(serde_json::from_reader(file)?)
848}
849
850#[cfg(test)]
851mod tests;