1use std::collections::BTreeMap;
2use std::ffi::OsString;
3use std::str::FromStr;
4use std::{cmp, env};
5
6use fedimint_core::util::SafeUrl;
7use fedimint_derive::{Decodable, Encodable};
8use fedimint_logging::LOG_CORE;
9use jsonrpsee_core::Serialize;
10use serde::Deserialize;
11use thiserror::Error;
12use tracing::warn;
13
14use crate::util::FmtCompact as _;
15
16pub const FM_USE_UNKNOWN_MODULE_ENV: &str = "FM_USE_UNKNOWN_MODULE";
19
20pub const FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV: &str =
23 "FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING";
24
25pub const FM_ENABLE_MODULE_LNV1_ENV: &str = "FM_ENABLE_MODULE_LNV1";
26pub const FM_ENABLE_MODULE_LNV2_ENV: &str = "FM_ENABLE_MODULE_LNV2";
27pub const FM_ENABLE_MODULE_MINT_ENV: &str = "FM_ENABLE_MODULE_MINT";
28pub const FM_ENABLE_MODULE_MINTV2_ENV: &str = "FM_ENABLE_MODULE_MINTV2";
29pub const FM_ENABLE_MODULE_WALLET_ENV: &str = "FM_ENABLE_MODULE_WALLET";
30pub const FM_ENABLE_MODULE_WALLETV2_ENV: &str = "FM_ENABLE_MODULE_WALLETV2";
31
32pub const FM_DISABLE_BASE_FEES_ENV: &str = "FM_DISABLE_BASE_FEES";
34
35pub const FM_DEBUG_SHOW_SECRETS_ENV: &str = "FM_DEBUG_SHOW_SECRETS";
37
38pub fn is_env_var_set(var: &str) -> bool {
41 let Some(val) = std::env::var_os(var) else {
42 return false;
43 };
44 match val.as_encoded_bytes() {
45 b"0" | b"false" => false,
46 b"1" | b"true" => true,
47 _ => {
48 warn!(
49 target: LOG_CORE,
50 %var,
51 val = %val.to_string_lossy(),
52 "Env var value invalid is invalid and ignored, assuming `true`"
53 );
54 true
55 }
56 }
57}
58
59pub fn is_env_var_set_opt(var: &str) -> Option<bool> {
63 let val = std::env::var_os(var)?;
64 match val.as_encoded_bytes() {
65 b"0" | b"false" => Some(false),
66 b"1" | b"true" => Some(true),
67 _ => {
68 warn!(
69 target: LOG_CORE,
70 %var,
71 val = %val.to_string_lossy(),
72 "Env var value invalid is invalid and ignored"
73 );
74 None
75 }
76 }
77}
78
79pub fn is_running_in_test_env() -> bool {
82 let unit_test = cfg!(test);
83
84 unit_test || is_env_var_set("NEXTEST") || is_env_var_set(FM_IN_DEVIMINT_ENV)
85}
86
87pub fn next_poll_delay(reached_all_peers: bool) -> std::time::Duration {
97 if is_running_in_test_env() {
98 std::time::Duration::from_secs(5)
99 } else if reached_all_peers {
100 std::time::Duration::from_secs(600)
101 } else {
102 std::time::Duration::from_secs(30)
103 }
104}
105
106pub fn is_automatic_consensus_version_voting_disabled() -> bool {
109 is_env_var_set(FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV)
110}
111
112#[macro_export]
114macro_rules! fedimint_build_code_version_env {
115 () => {
116 env!("FEDIMINT_BUILD_CODE_VERSION")
117 };
118}
119
120pub const FM_BITCOIN_RPC_KIND_ENV: &str = "FM_BITCOIN_RPC_KIND";
122pub const FM_BITCOIN_RPC_URL_ENV: &str = "FM_BITCOIN_RPC_URL";
124pub const FM_BITCOIN_POLLING_INTERVAL_SECS_ENV: &str = "FM_BITCOIN_POLLING_INTERVAL_SECS";
126
127pub const FM_DEFAULT_BITCOIN_RPC_KIND_ENV: &str = "FM_DEFAULT_BITCOIN_RPC_KIND";
130pub const FM_DEFAULT_BITCOIN_RPC_KIND_BAD_ENV: &str = "FM_DEFAULT_BITCOIND_RPC_KIND";
131pub const FM_DEFAULT_BITCOIN_RPC_URL_ENV: &str = "FM_DEFAULT_BITCOIN_RPC_URL";
134pub const FM_DEFAULT_BITCOIN_RPC_URL_BAD_ENV: &str = "FM_DEFAULT_BITCOIND_RPC_URL";
135
136pub const FM_FORCE_BITCOIN_RPC_KIND_ENV: &str = "FM_FORCE_BITCOIN_RPC_KIND";
138pub const FM_FORCE_BITCOIN_RPC_KIND_BAD_ENV: &str = "FM_FORCE_BITCOIND_RPC_BAD_KIND";
139pub const FM_FORCE_BITCOIN_RPC_URL_ENV: &str = "FM_FORCE_BITCOIN_RPC_URL";
141pub const FM_FORCE_BITCOIN_RPC_URL_BAD_ENV: &str = "FM_FORCE_BITCOIND_RPC_URL";
142
143pub const FM_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_IROH_CONNECT_OVERRIDES";
154
155pub const FM_GW_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES";
158
159pub const FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_IROH_CONNECT_OVERRIDES_PLAIN";
167
168pub const FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES_PLAIN";
171
172pub const FM_IROH_DNS_ENV: &str = "FM_IROH_DNS";
174
175pub const FM_IROH_RELAY_ENV: &str = "FM_IROH_RELAY";
177
178pub const FM_IROH_DHT_ENABLE_ENV: &str = "FM_IROH_DHT_ENABLE";
180
181pub const FM_IROH_N0_DISCOVERY_ENABLE_ENV: &str = "FM_IROH_N0_DISCOVERY_ENABLE";
183
184pub const FM_IROH_PKARR_RESOLVER_ENABLE_ENV: &str = "FM_IROH_PKARR_RESOLVER_ENABLE";
186
187pub const FM_IROH_PKARR_PUBLISHER_ENABLE_ENV: &str = "FM_IROH_PKARR_PUBLISHER_ENABLE";
189
190pub const FM_IROH_RELAYS_ENABLE_ENV: &str = "FM_IROH_RELAYS_ENABLE";
192
193pub const FM_PKARR_ENABLE_ENV: &str = "FM_PKARR_ENABLE";
195
196pub const FM_PKARR_DHT_ENABLE_ENV: &str = "FM_PKARR_DHT_ENABLE";
198
199pub const FM_PKARR_RELAYS_ENABLE_ENV: &str = "FM_PKARR_RELAYS_ENABLE";
201
202pub const FM_WS_API_CONNECT_OVERRIDES_ENV: &str = "FM_WS_API_CONNECT_OVERRIDES";
206
207pub const FM_IROH_API_SECRET_KEY_OVERRIDE_ENV: &str = "FM_IROH_API_SECRET_KEY_OVERRIDE";
208pub const FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV: &str = "FM_IROH_P2P_SECRET_KEY_OVERRIDE";
209
210pub const FM_WALLET_FEERATE_SOURCES_ENV: &str = "FM_WALLET_FEERATE_SOURCES";
220
221pub const FM_IN_DEVIMINT_ENV: &str = "FM_IN_DEVIMINT";
223
224#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
226pub struct BitcoinRpcConfig {
227 pub kind: String,
228 pub url: SafeUrl,
229}
230
231impl BitcoinRpcConfig {
232 pub fn get_defaults_from_env_vars() -> Result<Self, EnvParseError> {
233 Self::defaults_from_lookup(&|var: &str| env::var_os(var))
234 }
235
236 fn defaults_from_lookup<L>(lookup: &L) -> Result<Self, EnvParseError>
239 where
240 L: Fn(&str) -> Option<OsString>,
241 {
242 let (_, kind) = first_env_var_set(
243 BITCOIN_RPC_KIND_ENVS,
244 FM_DEFAULT_BITCOIN_RPC_KIND_ENV,
245 lookup,
246 )?;
247 let (url_var, url) =
248 first_env_var_set(BITCOIN_RPC_URL_ENVS, FM_DEFAULT_BITCOIN_RPC_URL_ENV, lookup)?;
249
250 Ok(Self {
251 kind,
252 url: url.parse().map_err(|source| EnvParseError::InvalidUrl {
253 var: url_var,
254 source,
255 })?,
256 })
257 }
258}
259
260const BITCOIN_RPC_KIND_ENVS: &[(&str, Option<&str>)] = &[
263 (FM_FORCE_BITCOIN_RPC_KIND_ENV, None),
264 (FM_DEFAULT_BITCOIN_RPC_KIND_ENV, None),
265 (
266 FM_BITCOIN_RPC_KIND_ENV,
267 Some(FM_DEFAULT_BITCOIN_RPC_KIND_ENV),
268 ),
269 (
270 FM_FORCE_BITCOIN_RPC_KIND_BAD_ENV,
271 Some(FM_FORCE_BITCOIN_RPC_KIND_ENV),
272 ),
273 (
274 FM_DEFAULT_BITCOIN_RPC_KIND_BAD_ENV,
275 Some(FM_DEFAULT_BITCOIN_RPC_KIND_ENV),
276 ),
277];
278
279const BITCOIN_RPC_URL_ENVS: &[(&str, Option<&str>)] = &[
282 (FM_FORCE_BITCOIN_RPC_URL_ENV, None),
283 (FM_DEFAULT_BITCOIN_RPC_URL_ENV, None),
284 (FM_BITCOIN_RPC_URL_ENV, Some(FM_DEFAULT_BITCOIN_RPC_URL_ENV)),
285 (
286 FM_FORCE_BITCOIN_RPC_URL_BAD_ENV,
287 Some(FM_FORCE_BITCOIN_RPC_URL_ENV),
288 ),
289 (
290 FM_DEFAULT_BITCOIN_RPC_URL_BAD_ENV,
291 Some(FM_DEFAULT_BITCOIN_RPC_URL_ENV),
292 ),
293];
294
295fn first_env_var_set<L>(
306 vars: &[(&'static str, Option<&'static str>)],
307 canonical: &'static str,
308 lookup: &L,
309) -> Result<(&'static str, String), EnvParseError>
310where
311 L: Fn(&str) -> Option<OsString>,
312{
313 for &(var, replacement) in vars {
314 let Some(value) = lookup(var) else {
315 continue;
316 };
317 let Ok(value) = value.into_string() else {
318 return Err(EnvParseError::NotUnicode { var });
319 };
320
321 if let Some(replacement) = replacement {
322 warn!(target: LOG_CORE, "{var} is obsolete, use {replacement} instead");
323 }
324
325 return Ok((var, value));
326 }
327
328 Err(EnvParseError::NotSet { var: canonical })
329}
330
331#[derive(Debug, Error)]
333#[non_exhaustive]
334pub enum EnvParseError {
335 #[error("Environment variable {var} (or one of its aliases) is not set")]
338 NotSet { var: &'static str },
339 #[error("Environment variable {var} is not valid Unicode")]
342 NotUnicode { var: &'static str },
343 #[error("Environment variable {var} is not a valid URL")]
345 InvalidUrl {
346 var: &'static str,
347 #[source]
348 source: url::ParseError,
349 },
350}
351
352pub fn parse_kv_list_from_env<K, V>(env: &str) -> BTreeMap<K, V>
353where
354 K: FromStr + cmp::Ord,
355 <K as FromStr>::Err: std::error::Error,
356 V: FromStr,
357 <V as FromStr>::Err: std::error::Error,
358{
359 let mut map = BTreeMap::new();
360 let Ok(env_value) = std::env::var(env) else {
361 return BTreeMap::new();
362 };
363 for kv in env_value.split(',') {
364 let kv = kv.trim();
365
366 if kv.is_empty() {
367 continue;
368 }
369
370 if let Some((k, v)) = kv.split_once('=') {
371 let Some(k) = K::from_str(k)
372 .inspect_err(|err| {
373 warn!(
374 target: LOG_CORE,
375 err = %err.fmt_compact(),
376 "Error parsing value"
377 );
378 })
379 .ok()
380 else {
381 continue;
382 };
383 let Some(v) = V::from_str(v)
384 .inspect_err(|err| {
385 warn!(
386 target: LOG_CORE,
387 err = %err.fmt_compact(),
388 "Error parsing value"
389 );
390 })
391 .ok()
392 else {
393 continue;
394 };
395
396 map.insert(k, v);
397 }
398 }
399
400 map
401}
402
403#[cfg(test)]
404mod tests;