Skip to main content

fedimint_core/
envs.rs

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
16/// In tests we want to routinely enable an extra unknown module to ensure
17/// all client code handles correct modules that client doesn't know about.
18pub const FM_USE_UNKNOWN_MODULE_ENV: &str = "FM_USE_UNKNOWN_MODULE";
19
20/// Disable automatic consensus version voting for testing and development
21/// environments
22pub 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
32/// Disable mint base fees for testing and development environments
33pub const FM_DISABLE_BASE_FEES_ENV: &str = "FM_DISABLE_BASE_FEES";
34
35/// Print sensitive secrets without redacting them. Use only for debugging.
36pub const FM_DEBUG_SHOW_SECRETS_ENV: &str = "FM_DEBUG_SHOW_SECRETS";
37
38/// Check if env variable is set and not equal `0` or `false` which are common
39/// ways to disable something.
40pub 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
59/// Check if env variable is set and not equal `0` or `false` which are common
60/// ways to disable a setting. `None` if env var not set at all, which allows
61/// handling the default value.
62pub 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
79/// Use to detect if running in a test environment, either `cargo test` or
80/// `devimint`.
81pub 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
87/// How long to wait before polling peers for their supported module consensus
88/// version again.
89///
90/// The first poll of a freshly started process races the peers' API servers
91/// binding and normally loses, so a round that failed to reach everyone is
92/// retried soon rather than after the full interval -- otherwise activation is
93/// dead for that interval after every restart. A peer that stays unreachable is
94/// then polled at the short interval indefinitely, which is a few requests per
95/// minute.
96pub 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
106/// Use to disable automatic consensus version voting for testing and
107/// development environments
108pub fn is_automatic_consensus_version_voting_disabled() -> bool {
109    is_env_var_set(FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV)
110}
111
112/// Get value of `FEDIMINT_BUILD_CODE_VERSION` at compile time
113#[macro_export]
114macro_rules! fedimint_build_code_version_env {
115    () => {
116        env!("FEDIMINT_BUILD_CODE_VERSION")
117    };
118}
119
120/// Env var for bitcoin RPC kind (obsolete, use FM_DEFAULT_* instead)
121pub const FM_BITCOIN_RPC_KIND_ENV: &str = "FM_BITCOIN_RPC_KIND";
122/// Env var for bitcoin URL (obsolete, use FM_DEFAULT_* instead)
123pub const FM_BITCOIN_RPC_URL_ENV: &str = "FM_BITCOIN_RPC_URL";
124/// Env var how often to poll bitcoin source
125pub const FM_BITCOIN_POLLING_INTERVAL_SECS_ENV: &str = "FM_BITCOIN_POLLING_INTERVAL_SECS";
126
127/// Env var for bitcoin RPC kind (default, used only as a default value for DKG
128/// config settings)
129pub 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";
131/// Env var for bitcoin URL (default, used only as a default value for DKG
132/// config settings)
133pub 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
136/// Env var for bitcoin RPC kind (forced, takes priority over config settings)
137pub 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";
139/// Env var for bitcoin URL (default, takes priority over config settings)
140pub 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
143/// Env var to override iroh connectivity, in the legacy iroh 0.35 `NodeTicket`
144/// format.
145///
146/// Comma separated key-value list
147/// (`<node_id>=<ticket>,<node_id>=<ticket>,...`). Only still read by pre-0.12
148/// binaries that link iroh 0.35; current binaries
149/// read [`FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV`] instead. iroh 1.0 no longer
150/// ships the `NodeTicket` format, so the override format had to become version
151/// agnostic; rather than overload this var with two incompatible formats, the
152/// new format lives under its own name and both are emitted side by side.
153pub const FM_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_IROH_CONNECT_OVERRIDES";
154
155/// Env var to override iroh connectivity, in the legacy iroh 0.35 `NodeTicket`
156/// format. Gateway counterpart of [`FM_IROH_CONNECT_OVERRIDES_ENV`].
157pub const FM_GW_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES";
158
159/// Env var to override iroh connectivity, in the plain `<id>=<addr>` format.
160///
161/// Comma separated key-value list (`<node_id>=<socket_addr>,...`). The value is
162/// a single direct address; the consumer rebuilds the iroh node/endpoint
163/// address from the id and address, so the format works on both iroh 0.35 and
164/// iroh 1.0. Current binaries read this; pre-0.12 binaries read the legacy
165/// [`FM_IROH_CONNECT_OVERRIDES_ENV`] instead.
166pub const FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_IROH_CONNECT_OVERRIDES_PLAIN";
167
168/// Env var to override iroh connectivity, in the plain `<id>=<addr>` format.
169/// Gateway counterpart of [`FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV`].
170pub const FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES_PLAIN";
171
172/// Env var to override iroh DNS server
173pub const FM_IROH_DNS_ENV: &str = "FM_IROH_DNS";
174
175/// Env var to override iroh relays server
176pub const FM_IROH_RELAY_ENV: &str = "FM_IROH_RELAY";
177
178/// Env var to enable Iroh's use of DHT
179pub const FM_IROH_DHT_ENABLE_ENV: &str = "FM_IROH_DHT_ENABLE";
180
181/// Env var to disable default n0 discovery
182pub const FM_IROH_N0_DISCOVERY_ENABLE_ENV: &str = "FM_IROH_N0_DISCOVERY_ENABLE";
183
184/// Env var to disable default pkarr resolver
185pub const FM_IROH_PKARR_RESOLVER_ENABLE_ENV: &str = "FM_IROH_PKARR_RESOLVER_ENABLE";
186
187/// Env var to disable default pkarr publisher
188pub const FM_IROH_PKARR_PUBLISHER_ENABLE_ENV: &str = "FM_IROH_PKARR_PUBLISHER_ENABLE";
189
190/// Env var to disable Iroh's use of relays
191pub const FM_IROH_RELAYS_ENABLE_ENV: &str = "FM_IROH_RELAYS_ENABLE";
192
193/// Env var to disable all pkarr publishing (enabled by default)
194pub const FM_PKARR_ENABLE_ENV: &str = "FM_PKARR_ENABLE";
195
196/// Env var to enable pkarr DHT publishing (disabled by default)
197pub const FM_PKARR_DHT_ENABLE_ENV: &str = "FM_PKARR_DHT_ENABLE";
198
199/// Env var to disable pkarr relay publishing (enabled by default)
200pub const FM_PKARR_RELAYS_ENABLE_ENV: &str = "FM_PKARR_RELAYS_ENABLE";
201
202/// Env var to override tcp api connectivity
203///
204/// Comma separated key-value list (`peer_id=url,peer_id=url`)
205pub 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
210/// List of json api endpoint sources to use as a source of
211/// fee rate estimation.
212///
213/// `;`-separated list of urls with part after `#`
214/// ("fragment") specifying jq filter to extract sats/vB fee rate.
215/// Eg. `https://mempool.space/api/v1/fees/recommended#.halfHourFee`
216///
217/// Note that `#` is a standalone separator and *not* parsed as a part of the
218/// Url. Which means there's no need to escape it.
219pub const FM_WALLET_FEERATE_SOURCES_ENV: &str = "FM_WALLET_FEERATE_SOURCES";
220
221/// `devimint` will set when code is running inside `devimint`
222pub const FM_IN_DEVIMINT_ENV: &str = "FM_IN_DEVIMINT";
223
224/// Configuration for the bitcoin RPC
225#[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    /// The defaults, reading the variables through `lookup` rather than from
237    /// the process environment.
238    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
260/// The environment variables holding the Bitcoin RPC kind, in lookup order,
261/// each paired with the variable that replaces it if it is deprecated.
262const 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
279/// The environment variables holding the Bitcoin RPC URL, in lookup order,
280/// each paired with the variable that replaces it if it is deprecated.
281const 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
295/// The value of the first variable of `vars` that `lookup` finds, together
296/// with the name of that variable, warning about the deprecated ones on the
297/// way.
298///
299/// `canonical` is the variable a user should set, reported when none of them
300/// is.
301///
302/// A variable that is set but does not hold valid Unicode fails even when a
303/// lower-priority variable holds a usable value, because silently ignoring a
304/// variable the user did set would surprise them.
305fn 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/// Failure to build a configuration value from environment variables.
332#[derive(Debug, Error)]
333#[non_exhaustive]
334pub enum EnvParseError {
335    /// None of the accepted variables is set; `var` is the canonical one to
336    /// set.
337    #[error("Environment variable {var} (or one of its aliases) is not set")]
338    NotSet { var: &'static str },
339    /// The variable is set but its value is not valid Unicode. A lower-priority
340    /// alias is not consulted once a variable is set.
341    #[error("Environment variable {var} is not valid Unicode")]
342    NotUnicode { var: &'static str },
343    /// The variable is set but is not a valid URL.
344    #[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;