fireblocks_config/
config.rs1#[cfg(feature = "gpg")]
2use gpgme::{Context, Protocol};
3use {
4 crate::{Error, OutputFormat, Result},
5 config::{Config, File, FileFormat},
6 serde::Deserialize,
7 std::{
8 collections::HashMap,
9 fs,
10 path::{Path, PathBuf},
11 str::FromStr,
12 time::Duration,
13 },
14};
15
16pub(crate) fn expand_tilde(path: &str) -> PathBuf {
17 if path.starts_with('~') {
18 match dirs::home_dir() {
19 Some(mut home) => {
20 home.push(&path[2..]);
21 home
22 }
23 None => PathBuf::from(path),
24 }
25 } else {
26 PathBuf::from(path)
27 }
28}
29
30#[derive(Clone, Debug, Default, Deserialize)]
31pub struct DisplayConfig {
32 pub output: OutputFormat,
33}
34
35fn deserialize_duration<'de, D>(deserializer: D) -> std::result::Result<Duration, D::Error>
37where
38 D: serde::Deserializer<'de>,
39{
40 let s = String::deserialize(deserializer)?;
41 let seconds = u64::from_str(&s)
42 .map_err(|_| serde::de::Error::custom(format!("Invalid duration: {s}")))?;
43 Ok(Duration::from_secs(seconds))
44}
45
46pub(crate) fn default_poll_timeout() -> Duration {
47 Duration::from_secs(180)
48}
49
50pub(crate) fn default_poll_interval() -> Duration {
51 Duration::from_secs(5)
52}
53
54pub(crate) fn default_broadcast() -> bool {
55 false
56}
57
58#[derive(Clone, Debug, Deserialize)]
59pub struct Signer {
60 #[serde(
61 default = "default_poll_timeout",
62 deserialize_with = "deserialize_duration"
63 )]
64 pub poll_timeout: Duration,
65 #[serde(
66 default = "default_poll_interval",
67 deserialize_with = "deserialize_duration"
68 )]
69 pub poll_interval: Duration,
70 pub vault: String,
72 #[serde(default = "default_broadcast")]
74 pub broadcast: bool,
75}
76
77impl Default for Signer {
78 fn default() -> Self {
79 Self {
80 poll_timeout: default_poll_timeout(),
81 poll_interval: default_poll_interval(),
82 vault: String::new(),
83 broadcast: default_broadcast(),
84 }
85 }
86}
87
88#[derive(Clone, Debug, Default, Deserialize)]
89pub struct FireblocksConfig {
90 pub api_key: String,
91 pub url: String,
92 pub secret_path: Option<PathBuf>,
93 pub secret: Option<String>,
94 #[serde(rename = "display", default)]
95 pub display_config: DisplayConfig,
96 pub signer: Signer,
97 #[serde(default)]
99 pub extra: HashMap<String, serde_json::Value>,
100 #[serde(default)]
102 pub debug: bool,
103
104 #[serde(default)]
105 pub mainnet: bool,
106}
107
108impl FireblocksConfig {
109 pub fn get_extra<T, K>(&self, key: K) -> Result<T>
111 where
112 T: serde::de::DeserializeOwned,
113 K: AsRef<str>,
114 {
115 let key_str = key.as_ref();
116 let value = self.extra.get(key_str).ok_or_else(|| Error::NotPresent {
117 key: key_str.to_string(),
118 })?;
119
120 serde_json::from_value(value.clone()).map_err(|e| {
121 Error::ConfigParseError(config::ConfigError::Message(format!(
122 "Failed to deserialize key '{key_str}': {e}"
123 )))
124 })
125 }
126
127 pub fn get_extra_duration<K>(&self, key: K) -> Result<Duration>
160 where
161 K: AsRef<str>,
162 {
163 let seconds: u64 = self.get_extra(key)?;
164 Ok(Duration::from_secs(seconds))
165 }
166
167 pub fn has_extra<K>(&self, key: K) -> bool
169 where
170 K: AsRef<str>,
171 {
172 self.extra.contains_key(key.as_ref())
173 }
174
175 pub fn get_key(&self) -> Result<Vec<u8>> {
176 if let Some(ref key) = self.secret {
178 return Ok(key.clone().into_bytes());
179 }
180
181 let path = self.secret_path.as_ref().ok_or(Error::MissingSecret)?;
183 let expanded_path = if path.starts_with("~") {
184 expand_tilde(&path.to_string_lossy())
185 } else {
186 path.clone()
187 };
188
189 #[cfg(feature = "gpg")]
190 if expanded_path
191 .extension()
192 .is_some_and(|ext| ext.eq_ignore_ascii_case("gpg"))
193 {
194 return self.decrypt_gpg_file(&expanded_path);
195 }
196
197 fs::read(&expanded_path).map_err(|e| Error::IOError {
199 source: e,
200 path: expanded_path.to_string_lossy().to_string(),
201 })
202 }
203
204 #[cfg(feature = "gpg")]
205 fn decrypt_gpg_file(&self, path: &Path) -> Result<Vec<u8>> {
206 let mut ctx = Context::from_protocol(Protocol::OpenPgp)?;
207
208 let mut input = fs::File::open(path).map_err(|e| Error::IOError {
209 source: e,
210 path: path.to_string_lossy().to_string(),
211 })?;
212
213 let mut output = Vec::new();
214 ctx.decrypt(&mut input, &mut output)?;
215
216 Ok(output)
217 }
218}
219impl FireblocksConfig {
220 pub fn new<P: AsRef<Path>>(cfg: P, cfg_overrides: &[P]) -> Result<Self> {
221 let cfg_path = cfg.as_ref();
222 log::debug!("using config {}", cfg_path.display());
223
224 let mut config_builder =
225 Config::builder().add_source(File::new(&cfg_path.to_string_lossy(), FileFormat::Toml));
226
227 for override_path in cfg_overrides {
229 let path = override_path.as_ref();
230 log::debug!("adding config override: {}", path.display());
231 config_builder = config_builder
232 .add_source(File::new(&path.to_string_lossy(), FileFormat::Toml).required(true));
233 }
234
235 config_builder = config_builder
237 .add_source(config::Environment::with_prefix("FIREBLOCKS").try_parsing(true));
238
239 let conf: Self = config_builder.build()?.try_deserialize()?;
240 log::trace!("loaded config {conf:#?}");
241 Ok(conf)
242 }
243
244 pub fn with_overrides<P: AsRef<Path>>(
245 cfg: P,
246 overrides: impl IntoIterator<Item = P>,
247 ) -> Result<Self> {
248 let override_vec: Vec<P> = overrides.into_iter().collect();
249 Self::new(cfg, &override_vec)
250 }
251
252 pub fn init() -> Result<Self> {
255 Self::init_with_profiles::<&str>(&[])
256 }
257
258 pub fn init_with_profiles<S: AsRef<str>>(profiles: &[S]) -> Result<Self> {
279 let config_dir = dirs::config_dir().ok_or(Error::XdgConfigNotFound)?;
280 let fireblocks_dir = config_dir.join("fireblocks");
281 let default_config = fireblocks_dir.join("default.toml");
282
283 if !default_config.exists() {
284 return Err(Error::ConfigNotFound(
285 default_config.to_string_lossy().to_string(),
286 ));
287 }
288
289 log::debug!("loading default config: {}", default_config.display());
290
291 let mut profile_configs = Vec::new();
292 for profile in profiles {
293 let profile_file = format!("{}.toml", profile.as_ref());
294 let profile_config = fireblocks_dir.join(&profile_file);
295 if profile_config.exists() {
296 log::debug!("adding profile config: {}", profile_config.display());
297 profile_configs.push(profile_config);
298 } else {
299 return Err(Error::ProfileConfigNotFound(profile_file));
300 }
301 }
302
303 Self::new(default_config, &profile_configs)
304 }
305}