fireblocks_config/
config.rs1#[cfg(feature = "gpg")]
2use gpgme::{Context, Protocol};
3#[cfg(feature = "xdg")]
4use microxdg::XdgApp;
5use {
6 crate::{Error, OutputFormat, Result},
7 config::{Config, File, FileFormat},
8 serde::Deserialize,
9 std::{
10 fs,
11 path::{Path, PathBuf},
12 },
13};
14
15fn expand_tilde(path: &str) -> PathBuf {
16 if path.starts_with('~') {
17 match dirs::home_dir() {
18 Some(mut home) => {
19 home.push(&path[2..]);
20 home
21 }
22 None => PathBuf::from(path),
23 }
24 } else {
25 PathBuf::from(path)
26 }
27}
28
29#[derive(Clone, Debug, Default, Deserialize)]
30pub struct DisplayConfig {
31 pub output: OutputFormat,
32}
33
34fn default_poll_timeout() -> u64 {
35 180
36}
37
38fn default_poll_interval() -> u64 {
39 5
40}
41
42#[derive(Clone, Debug, Default, Deserialize)]
43pub struct Signer {
44 #[serde(default = "default_poll_timeout")]
45 pub poll_timeout: u64,
46 #[serde(default = "default_poll_interval")]
47 pub poll_interval: u64,
48 pub vault: String,
50}
51
52#[derive(Clone, Debug, Default, Deserialize)]
53pub struct FireblocksConfig {
54 pub api_key: String,
55 pub url: String,
56 pub secret_path: Option<PathBuf>,
57 pub secret: Option<String>,
58 #[serde(rename = "display")]
59 pub display_config: DisplayConfig,
60 pub signer: Signer,
61}
62
63impl FireblocksConfig {
64 pub fn get_key(&self) -> Result<Vec<u8>> {
65 if let Some(ref key) = self.secret {
67 return Ok(key.clone().into_bytes());
68 }
69
70 let path = self.secret_path.as_ref().ok_or(Error::MissingSecret)?;
72 let expanded_path = if path.starts_with("~") {
73 expand_tilde(&path.to_string_lossy())
74 } else {
75 path.clone()
76 };
77
78 #[cfg(feature = "gpg")]
79 if expanded_path
80 .extension()
81 .is_some_and(|ext| ext.eq_ignore_ascii_case("gpg"))
82 {
83 return self.decrypt_gpg_file(&expanded_path);
84 }
85
86 fs::read(&expanded_path).map_err(|e| Error::IOError {
88 source: e,
89 path: expanded_path.to_string_lossy().to_string(),
90 })
91 }
92
93 #[cfg(feature = "gpg")]
94 fn decrypt_gpg_file(&self, path: &Path) -> Result<Vec<u8>> {
95 let mut ctx = Context::from_protocol(Protocol::OpenPgp)?;
96
97 let mut input = fs::File::open(path).map_err(|e| Error::IOError {
98 source: e,
99 path: path.to_string_lossy().to_string(),
100 })?;
101
102 let mut output = Vec::new();
103 ctx.decrypt(&mut input, &mut output)?;
104
105 Ok(output)
106 }
107}
108impl FireblocksConfig {
109 pub fn new<P: AsRef<Path>>(cfg: P, cfg_overrides: &[P]) -> Result<Self> {
110 let cfg_path = cfg.as_ref();
111 tracing::debug!("using config {}", cfg_path.display());
112
113 let mut config_builder =
114 Config::builder().add_source(File::new(&cfg_path.to_string_lossy(), FileFormat::Toml));
115
116 for override_path in cfg_overrides {
118 let path = override_path.as_ref();
119 tracing::debug!("adding config override: {}", path.display());
120 config_builder = config_builder
121 .add_source(File::new(&path.to_string_lossy(), FileFormat::Toml).required(true));
122 }
123
124 config_builder = config_builder
126 .add_source(config::Environment::with_prefix("FIREBLOCKS").try_parsing(true));
127
128 let conf: Self = config_builder.build()?.try_deserialize()?;
129 tracing::trace!("loaded config {conf:#?}");
130 Ok(conf)
131 }
132
133 pub fn with_overrides<P: AsRef<Path>>(
134 cfg: P,
135 overrides: impl IntoIterator<Item = P>,
136 ) -> Result<Self> {
137 let override_vec: Vec<P> = overrides.into_iter().collect();
138 Self::new(cfg, &override_vec)
139 }
140
141 #[cfg(feature = "xdg")]
144 pub fn init() -> Result<Self> {
145 Self::init_with_profiles(&[])
146 }
147
148 #[cfg(feature = "xdg")]
169 pub fn init_with_profiles(profiles: &[&str]) -> Result<Self> {
170 let xdg_app = XdgApp::new("fireblocks")?;
171 let default_config = xdg_app.app_config_file("default.toml")?;
172
173 tracing::debug!("loading default config: {}", default_config.display());
174
175 let mut profile_configs = Vec::new();
176 for profile in profiles {
177 let profile_file = format!("{profile}.toml");
178 let profile_config = xdg_app.app_config_file(&profile_file)?;
179 if profile_config.exists() {
180 tracing::debug!("adding profile config: {}", profile_config.display());
181 profile_configs.push(profile_config);
182 } else {
183 tracing::warn!("profile config not found: {}", profile_config.display());
184 }
185 }
186
187 Self::new(default_config, &profile_configs)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use {super::*, std::path::PathBuf};
194
195 #[ignore]
196 #[test]
197 fn test_gpg_config() -> anyhow::Result<()> {
198 let b = "examples/config-gpg.toml";
199 let cfg = FireblocksConfig::new(b, &[])?;
200 cfg.get_key()?;
201 Ok(())
202 }
203
204 #[test]
205 fn test_config() -> anyhow::Result<()> {
206 let b = "examples/default.toml";
207 let cfg = FireblocksConfig::new(b, &[])?;
208 assert_eq!("blah", cfg.api_key);
209 assert!(cfg.secret_path.is_some());
210 if let Some(p) = cfg.secret_path.as_ref() {
211 assert_eq!(PathBuf::from("examples/test.pem"), *p);
212 }
213 assert_eq!("https://sandbox-api.fireblocks.io/v1", cfg.url);
214 assert_eq!(OutputFormat::Table, cfg.display_config.output);
215 unsafe {
216 std::env::set_var("FIREBLOCKS_SECRET", "override");
217 }
218 let cfg = FireblocksConfig::new(b, &[])?;
219 assert!(cfg.secret.is_some());
220 assert_eq!(String::from("override").as_bytes(), cfg.get_key()?);
221 if let Some(ref k) = cfg.secret_path {
222 assert_eq!(PathBuf::from("examples/test.pem"), *k);
223 }
224
225 assert_eq!(cfg.signer.vault, "0");
226 unsafe {
227 std::env::remove_var("FIREBLOCKS_SECRET");
228 }
229 Ok(())
230 }
231
232 #[test]
233 fn test_config_override() -> anyhow::Result<()> {
234 let b = "examples/default.toml";
235 let cfg_override = "examples/override.toml";
236 let cfg = FireblocksConfig::with_overrides(b, vec![cfg_override])?;
237 assert_eq!("production", cfg.api_key);
238 assert!(cfg.secret_path.is_some());
239 if let Some(p) = cfg.secret_path.as_ref() {
240 assert_eq!(PathBuf::from("examples/test.pem"), *p);
241 }
242 assert_eq!("https://api.fireblocks.io/v1", cfg.url);
243 assert_eq!(OutputFormat::Table, cfg.display_config.output);
244 Ok(())
245 }
246
247 #[test]
248 fn test_embedded_key() -> anyhow::Result<()> {
249 let b = "examples/default.toml";
250 let cfg_override = "examples/embedded.toml";
251 let cfg = FireblocksConfig::new(b, &[cfg_override])?;
252 assert!(cfg.secret.is_some());
253 let secret = cfg.secret.unwrap();
254 assert_eq!(String::from("i am a secret").as_bytes(), secret.as_bytes());
255 Ok(())
256 }
257
258 #[cfg(feature = "xdg")]
259 #[test]
260 fn test_xdg_init() {
261 match FireblocksConfig::init() {
264 Ok(_) => {
265 }
267 Err(_) => {
268 }
271 }
272
273 match FireblocksConfig::init_with_profiles(&["test", "production"]) {
274 Ok(_) => {
275 }
277 Err(_) => {
278 }
280 }
281 }
282}