use std::collections::BTreeMap;
use std::ffi::OsString;
use std::str::FromStr;
use std::{cmp, env};
use fedimint_core::util::SafeUrl;
use fedimint_derive::{Decodable, Encodable};
use fedimint_logging::LOG_CORE;
use jsonrpsee_core::Serialize;
use serde::Deserialize;
use thiserror::Error;
use tracing::warn;
use crate::util::FmtCompact as _;
pub const FM_USE_UNKNOWN_MODULE_ENV: &str = "FM_USE_UNKNOWN_MODULE";
pub const FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV: &str =
"FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING";
pub const FM_ENABLE_MODULE_LNV1_ENV: &str = "FM_ENABLE_MODULE_LNV1";
pub const FM_ENABLE_MODULE_LNV2_ENV: &str = "FM_ENABLE_MODULE_LNV2";
pub const FM_ENABLE_MODULE_MINT_ENV: &str = "FM_ENABLE_MODULE_MINT";
pub const FM_ENABLE_MODULE_MINTV2_ENV: &str = "FM_ENABLE_MODULE_MINTV2";
pub const FM_ENABLE_MODULE_WALLET_ENV: &str = "FM_ENABLE_MODULE_WALLET";
pub const FM_ENABLE_MODULE_WALLETV2_ENV: &str = "FM_ENABLE_MODULE_WALLETV2";
pub const FM_DISABLE_BASE_FEES_ENV: &str = "FM_DISABLE_BASE_FEES";
pub const FM_DEBUG_SHOW_SECRETS_ENV: &str = "FM_DEBUG_SHOW_SECRETS";
pub fn is_env_var_set(var: &str) -> bool {
let Some(val) = std::env::var_os(var) else {
return false;
};
match val.as_encoded_bytes() {
b"0" | b"false" => false,
b"1" | b"true" => true,
_ => {
warn!(
target: LOG_CORE,
%var,
val = %val.to_string_lossy(),
"Env var value invalid is invalid and ignored, assuming `true`"
);
true
}
}
}
pub fn is_env_var_set_opt(var: &str) -> Option<bool> {
let val = std::env::var_os(var)?;
match val.as_encoded_bytes() {
b"0" | b"false" => Some(false),
b"1" | b"true" => Some(true),
_ => {
warn!(
target: LOG_CORE,
%var,
val = %val.to_string_lossy(),
"Env var value invalid is invalid and ignored"
);
None
}
}
}
pub fn is_running_in_test_env() -> bool {
let unit_test = cfg!(test);
unit_test || is_env_var_set("NEXTEST") || is_env_var_set(FM_IN_DEVIMINT_ENV)
}
pub fn next_poll_delay(reached_all_peers: bool) -> std::time::Duration {
if is_running_in_test_env() {
std::time::Duration::from_secs(5)
} else if reached_all_peers {
std::time::Duration::from_secs(600)
} else {
std::time::Duration::from_secs(30)
}
}
pub fn is_automatic_consensus_version_voting_disabled() -> bool {
is_env_var_set(FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV)
}
#[macro_export]
macro_rules! fedimint_build_code_version_env {
() => {
env!("FEDIMINT_BUILD_CODE_VERSION")
};
}
pub const FM_BITCOIN_RPC_KIND_ENV: &str = "FM_BITCOIN_RPC_KIND";
pub const FM_BITCOIN_RPC_URL_ENV: &str = "FM_BITCOIN_RPC_URL";
pub const FM_BITCOIN_POLLING_INTERVAL_SECS_ENV: &str = "FM_BITCOIN_POLLING_INTERVAL_SECS";
pub const FM_DEFAULT_BITCOIN_RPC_KIND_ENV: &str = "FM_DEFAULT_BITCOIN_RPC_KIND";
pub const FM_DEFAULT_BITCOIN_RPC_KIND_BAD_ENV: &str = "FM_DEFAULT_BITCOIND_RPC_KIND";
pub const FM_DEFAULT_BITCOIN_RPC_URL_ENV: &str = "FM_DEFAULT_BITCOIN_RPC_URL";
pub const FM_DEFAULT_BITCOIN_RPC_URL_BAD_ENV: &str = "FM_DEFAULT_BITCOIND_RPC_URL";
pub const FM_FORCE_BITCOIN_RPC_KIND_ENV: &str = "FM_FORCE_BITCOIN_RPC_KIND";
pub const FM_FORCE_BITCOIN_RPC_KIND_BAD_ENV: &str = "FM_FORCE_BITCOIND_RPC_BAD_KIND";
pub const FM_FORCE_BITCOIN_RPC_URL_ENV: &str = "FM_FORCE_BITCOIN_RPC_URL";
pub const FM_FORCE_BITCOIN_RPC_URL_BAD_ENV: &str = "FM_FORCE_BITCOIND_RPC_URL";
pub const FM_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_IROH_CONNECT_OVERRIDES";
pub const FM_GW_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES";
pub const FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_IROH_CONNECT_OVERRIDES_PLAIN";
pub const FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES_PLAIN";
pub const FM_IROH_DNS_ENV: &str = "FM_IROH_DNS";
pub const FM_IROH_RELAY_ENV: &str = "FM_IROH_RELAY";
pub const FM_IROH_DHT_ENABLE_ENV: &str = "FM_IROH_DHT_ENABLE";
pub const FM_IROH_N0_DISCOVERY_ENABLE_ENV: &str = "FM_IROH_N0_DISCOVERY_ENABLE";
pub const FM_IROH_PKARR_RESOLVER_ENABLE_ENV: &str = "FM_IROH_PKARR_RESOLVER_ENABLE";
pub const FM_IROH_PKARR_PUBLISHER_ENABLE_ENV: &str = "FM_IROH_PKARR_PUBLISHER_ENABLE";
pub const FM_IROH_RELAYS_ENABLE_ENV: &str = "FM_IROH_RELAYS_ENABLE";
pub const FM_PKARR_ENABLE_ENV: &str = "FM_PKARR_ENABLE";
pub const FM_PKARR_DHT_ENABLE_ENV: &str = "FM_PKARR_DHT_ENABLE";
pub const FM_PKARR_RELAYS_ENABLE_ENV: &str = "FM_PKARR_RELAYS_ENABLE";
pub const FM_WS_API_CONNECT_OVERRIDES_ENV: &str = "FM_WS_API_CONNECT_OVERRIDES";
pub const FM_IROH_API_SECRET_KEY_OVERRIDE_ENV: &str = "FM_IROH_API_SECRET_KEY_OVERRIDE";
pub const FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV: &str = "FM_IROH_P2P_SECRET_KEY_OVERRIDE";
pub const FM_WALLET_FEERATE_SOURCES_ENV: &str = "FM_WALLET_FEERATE_SOURCES";
pub const FM_IN_DEVIMINT_ENV: &str = "FM_IN_DEVIMINT";
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
pub struct BitcoinRpcConfig {
pub kind: String,
pub url: SafeUrl,
}
impl BitcoinRpcConfig {
pub fn get_defaults_from_env_vars() -> Result<Self, EnvParseError> {
Self::defaults_from_lookup(&|var: &str| env::var_os(var))
}
fn defaults_from_lookup<L>(lookup: &L) -> Result<Self, EnvParseError>
where
L: Fn(&str) -> Option<OsString>,
{
let (_, kind) = first_env_var_set(
BITCOIN_RPC_KIND_ENVS,
FM_DEFAULT_BITCOIN_RPC_KIND_ENV,
lookup,
)?;
let (url_var, url) =
first_env_var_set(BITCOIN_RPC_URL_ENVS, FM_DEFAULT_BITCOIN_RPC_URL_ENV, lookup)?;
Ok(Self {
kind,
url: url.parse().map_err(|source| EnvParseError::InvalidUrl {
var: url_var,
source,
})?,
})
}
}
const BITCOIN_RPC_KIND_ENVS: &[(&str, Option<&str>)] = &[
(FM_FORCE_BITCOIN_RPC_KIND_ENV, None),
(FM_DEFAULT_BITCOIN_RPC_KIND_ENV, None),
(
FM_BITCOIN_RPC_KIND_ENV,
Some(FM_DEFAULT_BITCOIN_RPC_KIND_ENV),
),
(
FM_FORCE_BITCOIN_RPC_KIND_BAD_ENV,
Some(FM_FORCE_BITCOIN_RPC_KIND_ENV),
),
(
FM_DEFAULT_BITCOIN_RPC_KIND_BAD_ENV,
Some(FM_DEFAULT_BITCOIN_RPC_KIND_ENV),
),
];
const BITCOIN_RPC_URL_ENVS: &[(&str, Option<&str>)] = &[
(FM_FORCE_BITCOIN_RPC_URL_ENV, None),
(FM_DEFAULT_BITCOIN_RPC_URL_ENV, None),
(FM_BITCOIN_RPC_URL_ENV, Some(FM_DEFAULT_BITCOIN_RPC_URL_ENV)),
(
FM_FORCE_BITCOIN_RPC_URL_BAD_ENV,
Some(FM_FORCE_BITCOIN_RPC_URL_ENV),
),
(
FM_DEFAULT_BITCOIN_RPC_URL_BAD_ENV,
Some(FM_DEFAULT_BITCOIN_RPC_URL_ENV),
),
];
fn first_env_var_set<L>(
vars: &[(&'static str, Option<&'static str>)],
canonical: &'static str,
lookup: &L,
) -> Result<(&'static str, String), EnvParseError>
where
L: Fn(&str) -> Option<OsString>,
{
for &(var, replacement) in vars {
let Some(value) = lookup(var) else {
continue;
};
let Ok(value) = value.into_string() else {
return Err(EnvParseError::NotUnicode { var });
};
if let Some(replacement) = replacement {
warn!(target: LOG_CORE, "{var} is obsolete, use {replacement} instead");
}
return Ok((var, value));
}
Err(EnvParseError::NotSet { var: canonical })
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EnvParseError {
#[error("Environment variable {var} (or one of its aliases) is not set")]
NotSet { var: &'static str },
#[error("Environment variable {var} is not valid Unicode")]
NotUnicode { var: &'static str },
#[error("Environment variable {var} is not a valid URL")]
InvalidUrl {
var: &'static str,
#[source]
source: url::ParseError,
},
}
pub fn parse_kv_list_from_env<K, V>(env: &str) -> BTreeMap<K, V>
where
K: FromStr + cmp::Ord,
<K as FromStr>::Err: std::error::Error,
V: FromStr,
<V as FromStr>::Err: std::error::Error,
{
let mut map = BTreeMap::new();
let Ok(env_value) = std::env::var(env) else {
return BTreeMap::new();
};
for kv in env_value.split(',') {
let kv = kv.trim();
if kv.is_empty() {
continue;
}
if let Some((k, v)) = kv.split_once('=') {
let Some(k) = K::from_str(k)
.inspect_err(|err| {
warn!(
target: LOG_CORE,
err = %err.fmt_compact(),
"Error parsing value"
);
})
.ok()
else {
continue;
};
let Some(v) = V::from_str(v)
.inspect_err(|err| {
warn!(
target: LOG_CORE,
err = %err.fmt_compact(),
"Error parsing value"
);
})
.ok()
else {
continue;
};
map.insert(k, v);
}
}
map
}
#[cfg(test)]
mod tests;