fedimint-core 0.13.0-beta.0

fedimint-core provides common code used by both client and server.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
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 _;

/// In tests we want to routinely enable an extra unknown module to ensure
/// all client code handles correct modules that client doesn't know about.
pub const FM_USE_UNKNOWN_MODULE_ENV: &str = "FM_USE_UNKNOWN_MODULE";

/// Disable automatic consensus version voting for testing and development
/// environments
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";

/// Disable mint base fees for testing and development environments
pub const FM_DISABLE_BASE_FEES_ENV: &str = "FM_DISABLE_BASE_FEES";

/// Print sensitive secrets without redacting them. Use only for debugging.
pub const FM_DEBUG_SHOW_SECRETS_ENV: &str = "FM_DEBUG_SHOW_SECRETS";

/// Check if env variable is set and not equal `0` or `false` which are common
/// ways to disable something.
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
        }
    }
}

/// Check if env variable is set and not equal `0` or `false` which are common
/// ways to disable a setting. `None` if env var not set at all, which allows
/// handling the default value.
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
        }
    }
}

/// Use to detect if running in a test environment, either `cargo test` or
/// `devimint`.
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)
}

/// How long to wait before polling peers for their supported module consensus
/// version again.
///
/// The first poll of a freshly started process races the peers' API servers
/// binding and normally loses, so a round that failed to reach everyone is
/// retried soon rather than after the full interval -- otherwise activation is
/// dead for that interval after every restart. A peer that stays unreachable is
/// then polled at the short interval indefinitely, which is a few requests per
/// minute.
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)
    }
}

/// Use to disable automatic consensus version voting for testing and
/// development environments
pub fn is_automatic_consensus_version_voting_disabled() -> bool {
    is_env_var_set(FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV)
}

/// Get value of `FEDIMINT_BUILD_CODE_VERSION` at compile time
#[macro_export]
macro_rules! fedimint_build_code_version_env {
    () => {
        env!("FEDIMINT_BUILD_CODE_VERSION")
    };
}

/// Env var for bitcoin RPC kind (obsolete, use FM_DEFAULT_* instead)
pub const FM_BITCOIN_RPC_KIND_ENV: &str = "FM_BITCOIN_RPC_KIND";
/// Env var for bitcoin URL (obsolete, use FM_DEFAULT_* instead)
pub const FM_BITCOIN_RPC_URL_ENV: &str = "FM_BITCOIN_RPC_URL";
/// Env var how often to poll bitcoin source
pub const FM_BITCOIN_POLLING_INTERVAL_SECS_ENV: &str = "FM_BITCOIN_POLLING_INTERVAL_SECS";

/// Env var for bitcoin RPC kind (default, used only as a default value for DKG
/// config settings)
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";
/// Env var for bitcoin URL (default, used only as a default value for DKG
/// config settings)
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";

/// Env var for bitcoin RPC kind (forced, takes priority over config settings)
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";
/// Env var for bitcoin URL (default, takes priority over config settings)
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";

/// Env var to override iroh connectivity, in the legacy iroh 0.35 `NodeTicket`
/// format.
///
/// Comma separated key-value list
/// (`<node_id>=<ticket>,<node_id>=<ticket>,...`). Only still read by pre-0.12
/// binaries that link iroh 0.35; current binaries
/// read [`FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV`] instead. iroh 1.0 no longer
/// ships the `NodeTicket` format, so the override format had to become version
/// agnostic; rather than overload this var with two incompatible formats, the
/// new format lives under its own name and both are emitted side by side.
pub const FM_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_IROH_CONNECT_OVERRIDES";

/// Env var to override iroh connectivity, in the legacy iroh 0.35 `NodeTicket`
/// format. Gateway counterpart of [`FM_IROH_CONNECT_OVERRIDES_ENV`].
pub const FM_GW_IROH_CONNECT_OVERRIDES_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES";

/// Env var to override iroh connectivity, in the plain `<id>=<addr>` format.
///
/// Comma separated key-value list (`<node_id>=<socket_addr>,...`). The value is
/// a single direct address; the consumer rebuilds the iroh node/endpoint
/// address from the id and address, so the format works on both iroh 0.35 and
/// iroh 1.0. Current binaries read this; pre-0.12 binaries read the legacy
/// [`FM_IROH_CONNECT_OVERRIDES_ENV`] instead.
pub const FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_IROH_CONNECT_OVERRIDES_PLAIN";

/// Env var to override iroh connectivity, in the plain `<id>=<addr>` format.
/// Gateway counterpart of [`FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV`].
pub const FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV: &str = "FM_GW_IROH_CONNECT_OVERRIDES_PLAIN";

/// Env var to override iroh DNS server
pub const FM_IROH_DNS_ENV: &str = "FM_IROH_DNS";

/// Env var to override iroh relays server
pub const FM_IROH_RELAY_ENV: &str = "FM_IROH_RELAY";

/// Env var to enable Iroh's use of DHT
pub const FM_IROH_DHT_ENABLE_ENV: &str = "FM_IROH_DHT_ENABLE";

/// Env var to disable default n0 discovery
pub const FM_IROH_N0_DISCOVERY_ENABLE_ENV: &str = "FM_IROH_N0_DISCOVERY_ENABLE";

/// Env var to disable default pkarr resolver
pub const FM_IROH_PKARR_RESOLVER_ENABLE_ENV: &str = "FM_IROH_PKARR_RESOLVER_ENABLE";

/// Env var to disable default pkarr publisher
pub const FM_IROH_PKARR_PUBLISHER_ENABLE_ENV: &str = "FM_IROH_PKARR_PUBLISHER_ENABLE";

/// Env var to disable Iroh's use of relays
pub const FM_IROH_RELAYS_ENABLE_ENV: &str = "FM_IROH_RELAYS_ENABLE";

/// Env var to disable all pkarr publishing (enabled by default)
pub const FM_PKARR_ENABLE_ENV: &str = "FM_PKARR_ENABLE";

/// Env var to enable pkarr DHT publishing (disabled by default)
pub const FM_PKARR_DHT_ENABLE_ENV: &str = "FM_PKARR_DHT_ENABLE";

/// Env var to disable pkarr relay publishing (enabled by default)
pub const FM_PKARR_RELAYS_ENABLE_ENV: &str = "FM_PKARR_RELAYS_ENABLE";

/// Env var to override tcp api connectivity
///
/// Comma separated key-value list (`peer_id=url,peer_id=url`)
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";

/// List of json api endpoint sources to use as a source of
/// fee rate estimation.
///
/// `;`-separated list of urls with part after `#`
/// ("fragment") specifying jq filter to extract sats/vB fee rate.
/// Eg. `https://mempool.space/api/v1/fees/recommended#.halfHourFee`
///
/// Note that `#` is a standalone separator and *not* parsed as a part of the
/// Url. Which means there's no need to escape it.
pub const FM_WALLET_FEERATE_SOURCES_ENV: &str = "FM_WALLET_FEERATE_SOURCES";

/// `devimint` will set when code is running inside `devimint`
pub const FM_IN_DEVIMINT_ENV: &str = "FM_IN_DEVIMINT";

/// Configuration for the bitcoin RPC
#[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))
    }

    /// The defaults, reading the variables through `lookup` rather than from
    /// the process environment.
    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,
            })?,
        })
    }
}

/// The environment variables holding the Bitcoin RPC kind, in lookup order,
/// each paired with the variable that replaces it if it is deprecated.
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),
    ),
];

/// The environment variables holding the Bitcoin RPC URL, in lookup order,
/// each paired with the variable that replaces it if it is deprecated.
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),
    ),
];

/// The value of the first variable of `vars` that `lookup` finds, together
/// with the name of that variable, warning about the deprecated ones on the
/// way.
///
/// `canonical` is the variable a user should set, reported when none of them
/// is.
///
/// A variable that is set but does not hold valid Unicode fails even when a
/// lower-priority variable holds a usable value, because silently ignoring a
/// variable the user did set would surprise them.
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 })
}

/// Failure to build a configuration value from environment variables.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EnvParseError {
    /// None of the accepted variables is set; `var` is the canonical one to
    /// set.
    #[error("Environment variable {var} (or one of its aliases) is not set")]
    NotSet { var: &'static str },
    /// The variable is set but its value is not valid Unicode. A lower-priority
    /// alias is not consulted once a variable is set.
    #[error("Environment variable {var} is not valid Unicode")]
    NotUnicode { var: &'static str },
    /// The variable is set but is not a valid URL.
    #[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;