Skip to main content

fedimintd/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_wrap)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::must_use_candidate)]
6#![allow(clippy::return_self_not_must_use)]
7#![allow(clippy::large_futures)]
8
9mod metrics;
10
11use std::convert::Infallible;
12use std::env;
13use std::fmt::Write as _;
14use std::net::SocketAddr;
15use std::path::PathBuf;
16use std::time::Duration;
17
18use anyhow::Context as _;
19use bitcoin::Network;
20use clap::builder::BoolishValueParser;
21use clap::{ArgGroup, CommandFactory, FromArgMatches, Parser};
22use fedimint_core::db::Database;
23use fedimint_core::envs::{
24    FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
25    is_running_in_test_env,
26};
27use fedimint_core::module::registry::ModuleRegistry;
28use fedimint_core::module::{ApiAuth, CORE_CONSENSUS_VERSION};
29use fedimint_core::rustls::install_crypto_provider;
30use fedimint_core::task::TaskGroup;
31use fedimint_core::timing;
32use fedimint_core::util::{FmtCompact as _, SafeUrl, handle_version_hash_command};
33use fedimint_ln_server::LightningInit;
34use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
35use fedimint_meta_server::MetaInit;
36use fedimint_mint_server::MintInit;
37use fedimint_rocksdb::RocksDb;
38use fedimint_server::IrohNextApiSettings;
39use fedimint_server::config::ConfigGenSettings;
40use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD};
41use fedimint_server::core::ServerModuleInitRegistry;
42use fedimint_server::net::api::ApiSecrets;
43use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
44use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
45use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
46use fedimint_server_bitcoin_rpc::tracked::ServerBitcoinRpcTracked;
47use fedimint_server_core::ServerModuleInitRegistryExt;
48use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
49use fedimint_unknown_server::UnknownInit;
50use fedimint_wallet_server::WalletInit;
51use fedimintd_envs::{
52    FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_API_NEXT_ENV, FM_BIND_METRICS_ENV, FM_BIND_P2P_ENV,
53    FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_PASSWORD_ENV,
54    FM_BITCOIND_URL_ENV, FM_BITCOIND_URL_PASSWORD_FILE_ENV, FM_BITCOIND_USERNAME_ENV,
55    FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
56    FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
57    FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV,
58    FM_IROH_NEXT_ENABLE_ENV, FM_IROH_P2P_RELAY_ENV, FM_P2P_MAX_CONNECTION_AGE_SECS_ENV,
59    FM_P2P_URL_ENV, FM_PASSWORD_API_ENV, FM_PASSWORD_UI_ENV, FM_SESSION_TIMEOUT_SECS_ENV,
60};
61use futures::FutureExt as _;
62#[cfg(all(
63    not(feature = "jemalloc"),
64    not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
65))]
66use tracing::warn;
67use tracing::{debug, error, info};
68
69use crate::metrics::APP_START_TS;
70
71/// Time we will wait before forcefully shutting down tasks
72const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
73
74#[derive(Parser)]
75#[command(version)]
76#[command(
77    group(
78        ArgGroup::new("bitcoind_password_auth")
79           .args(["bitcoind_password", "bitcoind_url_password_file"])
80           .multiple(false)
81    ),
82    group(
83        ArgGroup::new("bitcoind_auth")
84            .args(["bitcoind_url"])
85            .requires("bitcoind_password_auth")
86            .requires_all(["bitcoind_username", "bitcoind_url"])
87    ),
88    group(
89        ArgGroup::new("bitcoin_rpc")
90            .required(true)
91            .multiple(true)
92            .args(["bitcoind_url", "esplora_url"])
93    )
94)]
95struct ServerOpts {
96    /// Path to folder containing federation config files
97    #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
98    data_dir: PathBuf,
99
100    /// Password gating the guardian admin UI on bind-ui. Optional: falls
101    /// back to reading password.private from data-dir, and if neither is
102    /// present the UI is served without a login form. Only safe when
103    /// bind-ui stays on a trusted interface (the default 127.0.0.1).
104    #[arg(long, env = FM_PASSWORD_UI_ENV)]
105    password_ui: Option<String>,
106
107    /// Password gating admin RPCs on the public API (WebSocket on bind-api
108    /// and iroh). Optional and never falls back to disk: when unset, admin
109    /// RPCs return 401 unconditionally, since the public API is always
110    /// network-reachable and must be enabled explicitly.
111    #[arg(long, env = FM_PASSWORD_API_ENV)]
112    password_api: Option<String>,
113
114    /// The bitcoin network of the federation
115    #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
116    bitcoin_network: Network,
117
118    /// The username to use when connecting to bitcoind
119    #[arg(long, env = FM_BITCOIND_USERNAME_ENV)]
120    bitcoind_username: Option<String>,
121
122    /// The password to use when connecting to bitcoind
123    #[arg(long, env = FM_BITCOIND_PASSWORD_ENV)]
124    bitcoind_password: Option<String>,
125
126    /// Bitcoind RPC URL, e.g. <http://127.0.0.1:8332>
127    /// This should not include authentication parameters, they should be
128    /// included in `FM_BITCOIND_USERNAME` and `FM_BITCOIND_PASSWORD`
129    ///
130    /// With Esplora configured, reads remain bitcoind-first except block count
131    /// uses Esplora while bitcoind reports initial block download. Broadcast
132    /// remains bitcoind-first.
133    #[arg(long, env = FM_BITCOIND_URL_ENV)]
134    bitcoind_url: Option<SafeUrl>,
135
136    /// If set, the password part of `--bitcoind-url` will be set/replaced with
137    /// the content of this file.
138    ///
139    /// This is useful for setups that provide secret material via ramdisk
140    /// e.g. SOPS, age, etc.
141    ///
142    /// Note this is not meant to handle bitcoind's cookie file.
143    #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
144    bitcoind_url_password_file: Option<PathBuf>,
145
146    /// Trusted Esplora HTTP base URL, e.g. <https://mempool.space/api>
147    ///
148    /// With bitcoind configured, retries individual failed reads and supplies
149    /// block count while bitcoind reports initial block download. Their chain
150    /// identities are compared once at startup when both respond. Esplora can
151    /// bootstrap while bitcoind is offline and is trusted for chain selection,
152    /// not independently verified.
153    /// Transactions go to Esplora only after a bitcoind broadcast error.
154    /// See SECURITY.md for the trust model.
155    #[arg(long, env = FM_ESPLORA_URL_ENV)]
156    esplora_url: Option<SafeUrl>,
157
158    /// Address we bind to for p2p consensus communication
159    ///
160    /// Should be `0.0.0.0:8173` most of the time, as p2p connectivity is public
161    /// and direct, and the port should be open it in the firewall.
162    #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
163    bind_p2p: SocketAddr,
164
165    /// Address we bind to for the API
166    ///
167    /// Should be `0.0.0.0:8174` most of the time, as api connectivity is public
168    /// and direct, and the port should be open it in the firewall.
169    #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
170    bind_api: SocketAddr,
171
172    /// Address we bind to for exposing the Web UI
173    ///
174    /// Built-in web UI is exposed as an HTTP port, and typically should
175    /// have TLS terminated by Nginx/Traefik/etc. and forwarded to the locally
176    /// bind port.
177    #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
178    bind_ui: SocketAddr,
179
180    /// Our external address for communicating with our peers
181    ///
182    /// `fedimint://<fqdn>:8173` for TCP/TLS p2p connectivity (legacy/standard).
183    ///
184    /// Ignored when Iroh stack is used. (newer/experimental)
185    #[arg(long, env = FM_P2P_URL_ENV)]
186    p2p_url: Option<SafeUrl>,
187
188    /// Our API address for clients to connect to us
189    ///
190    /// Typically `wss://<fqdn>/ws/` for TCP/TLS connectivity (legacy/standard)
191    ///
192    /// Ignored when Iroh stack is used. (newer/experimental)
193    #[arg(long, env = FM_API_URL_ENV)]
194    api_url: Option<SafeUrl>,
195
196    /// Whether to use the Iroh networking stack instead of the legacy
197    /// TCP/TLS/websocket stack. Defaults to Iroh in production and to the
198    /// legacy stack in test environments (`cargo test` / `devimint`). Only
199    /// consulted at DKG / config generation time; existing federations keep the
200    /// stack baked into their config. Pass `--enable-iroh true/false` (or
201    /// `FM_ENABLE_IROH=true/false`) to override.
202    #[arg(long, env = FM_ENABLE_IROH_ENV, value_parser = BoolishValueParser::new())]
203    enable_iroh: Option<bool>,
204
205    /// Optional URL of the Iroh DNS server
206    #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
207    iroh_dns: Option<SafeUrl>,
208
209    /// Optional URLs of the Iroh relays to use for registering
210    #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh", value_delimiter = ',')]
211    iroh_relays: Vec<SafeUrl>,
212
213    /// Optional Iroh 1.0 relay URLs to use for guardian P2P
214    #[arg(long, env = FM_IROH_P2P_RELAY_ENV, value_delimiter = ',')]
215    iroh_p2p_relays: Vec<SafeUrl>,
216
217    /// Number of checkpoints from the current session to retain on disk
218    #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
219    db_checkpoint_retention: u64,
220
221    /// Exit the process if a consensus session is not completed within this
222    /// many seconds, relying on the process supervisor to restart fedimintd.
223    /// Restarting guardians has been observed to resolve stuck sessions.
224    #[arg(long, env = FM_SESSION_TIMEOUT_SECS_ENV, default_value = "3600")]
225    session_timeout_secs: u64,
226
227    /// Drop a p2p connection to a peer once it exceeds this age in seconds,
228    /// relying on the reconnection logic to re-establish it. Unset by
229    /// default, in which case connections are never dropped due to their age.
230    #[arg(long, env = FM_P2P_MAX_CONNECTION_AGE_SECS_ENV)]
231    p2p_max_connection_age_secs: Option<u64>,
232
233    /// Enable tokio console logging
234    #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
235    bind_tokio_console: Option<SocketAddr>,
236
237    /// Enable jaeger for tokio console logging
238    #[arg(long, default_value = "false")]
239    with_jaeger: bool,
240
241    /// Enable prometheus metrics
242    #[arg(long, env = FM_BIND_METRICS_ENV, default_value = "127.0.0.1:8176")]
243    bind_metrics: Option<SocketAddr>,
244
245    /// Comma separated list of API secrets.
246    ///
247    /// Setting it will enforce API authentication and make the Federation
248    /// "private".
249    ///
250    /// The first secret in the list is the "active" one that the peer will use
251    /// itself to connect to other peers. Any further one is accepted by
252    /// this peer, e.g. for the purposes of smooth rotation of secret
253    /// between users.
254    ///
255    /// Note that the value provided here will override any other settings
256    /// that the user might want to set via UI at runtime, etc.
257    /// In the future, managing secrets might be possible via Admin UI
258    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
259    #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
260    force_api_secrets: ApiSecrets,
261
262    /// Maximum number of concurrent Iroh API connections
263    #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
264    iroh_api_max_connections: usize,
265
266    /// Maximum number of parallel requests per Iroh API connection
267    #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
268    iroh_api_max_requests_per_connection: usize,
269
270    /// Enable the transitional Iroh 1.0 API endpoint alongside Iroh 0.35.
271    ///
272    /// For a federation configured with the legacy Iroh API, this is a runtime
273    /// setting independent of the DKG-only `--enable-iroh` option. It is
274    /// enabled by default. Once the endpoint is advertised, it must remain
275    /// enabled.
276    #[arg(
277        long,
278        env = FM_IROH_NEXT_ENABLE_ENV,
279        default_value_t = true,
280        action = clap::ArgAction::Set,
281        num_args = 0..=1,
282        default_missing_value = "true",
283    )]
284    enable_iroh_next: bool,
285
286    /// Bind address for the transitional Iroh 1.0 API endpoint
287    #[arg(long, env = FM_BIND_API_NEXT_ENV)]
288    bind_api_next: Option<SocketAddr>,
289}
290
291impl ServerOpts {
292    pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
293        let url = self
294            .bitcoind_url
295            .clone()
296            .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
297        if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
298            let password = tokio::fs::read_to_string(password_file)
299                .await
300                .context("Failed to read the password")?
301                .trim()
302                .to_owned();
303            Ok((url, password))
304        } else {
305            let password = self
306                .bitcoind_password
307                .clone()
308                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
309            Ok((url, password))
310        }
311    }
312}
313
314/// Block the thread and run a Fedimintd server
315///
316/// # Arguments
317///
318/// * `module_init_registry` - The registry of available modules.
319///
320/// * `code_version_hash` - The git hash of the code that the `fedimintd` binary
321///   is being built from. This is used mostly for information purposes
322///   (`fedimintd version-hash`). See `fedimint-build` crate for easy way to
323///   obtain it.
324///
325/// * `code_version_vendor_suffix` - An optional vendor string appended to the
326///   internal Fedimint release version, to distinguish binaries built by
327///   different vendors, usually with a different set of modules. It is encoded
328///   as `SemVer` build metadata and therefore must contain dot-separated,
329///   non-empty identifiers with only ASCII alphanumeric characters and hyphens.
330///   Setup/DKG compatibility and consensus config generation require the same
331///   exact optional vendor string as well as the same `major.minor` series.
332#[allow(clippy::too_many_lines)]
333pub async fn run(
334    module_init_registry: ServerModuleInitRegistry,
335    code_version_hash: &str,
336    code_version_vendor_suffix: Option<&str>,
337) -> anyhow::Result<Infallible> {
338    assert_eq!(
339        env!("FEDIMINT_BUILD_CODE_VERSION").len(),
340        code_version_hash.len(),
341        "version_hash must have an expected length"
342    );
343
344    handle_version_hash_command(code_version_hash);
345
346    let fedimint_version = env!("CARGO_PKG_VERSION");
347
348    APP_START_TS
349        .with_label_values(&[fedimint_version, code_version_hash])
350        .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
351
352    let server_opts = {
353        // Collect env vars from all registered modules and append them to the
354        // long-help text so operators can discover them via `fedimintd --help`.
355        let mut module_env_help = String::from("\nModule environment variables:\n");
356        for (_kind, module_init) in module_init_registry.iter() {
357            for doc in module_init.get_documented_env_vars() {
358                let _ = writeln!(module_env_help, "  {:40}  {}", doc.name, doc.description);
359            }
360        }
361        let matches = ServerOpts::command()
362            .after_long_help(module_env_help)
363            .get_matches();
364        ServerOpts::from_arg_matches(&matches)
365            .expect("clap arg matches must be valid after parsing")
366    };
367
368    let mut tracing_builder = TracingSetup::default();
369
370    tracing_builder
371        .tokio_console_bind(server_opts.bind_tokio_console)
372        .with_jaeger(server_opts.with_jaeger);
373
374    tracing_builder.init().map_err(anyhow::Error::from).unwrap();
375
376    info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
377
378    #[cfg(all(
379        not(feature = "jemalloc"),
380        not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
381    ))]
382    warn!(
383        target: LOG_SERVER,
384        "fedimintd was built without the `jemalloc` feature. rocksdb is prone to memory \
385         fragmentation with the default allocator; consider rebuilding with `--features jemalloc`."
386    );
387
388    debug!(
389        target: LOG_SERVER,
390        core_consensus = %CORE_CONSENSUS_VERSION,
391        "Supported core consensus version",
392    );
393
394    let code_version_str = code_version_vendor_suffix.map_or_else(
395        || fedimint_version.to_string(),
396        |suffix| format!("{fedimint_version}+{suffix}"),
397    );
398    fedimint_core::version::DkgVersion::parse(&code_version_str)
399        .context("Invalid Fedimint version vendor string")?;
400
401    let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
402
403    let root_task_group = TaskGroup::new();
404
405    if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
406        info!(
407            target: LOG_SERVER,
408            url = %format!("http://{}/metrics", bind_metrics),
409            "Initializing metrics server",
410        );
411        fedimint_metrics::spawn_api_server(*bind_metrics, root_task_group.clone()).await?;
412    }
413
414    let enable_iroh = server_opts.enable_iroh.unwrap_or(!is_running_in_test_env());
415    let iroh_next_api_settings = if server_opts.enable_iroh_next {
416        Some(IrohNextApiSettings::new(server_opts.bind_api_next))
417    } else {
418        None
419    };
420
421    let settings = ConfigGenSettings {
422        p2p_bind: server_opts.bind_p2p,
423        api_bind: server_opts.bind_api,
424        ui_bind: server_opts.bind_ui,
425        p2p_url: server_opts.p2p_url.clone(),
426        api_url: server_opts.api_url.clone(),
427        enable_iroh,
428        iroh_dns: server_opts.iroh_dns.clone(),
429        iroh_relays: server_opts.iroh_relays.clone(),
430        network: server_opts.bitcoin_network,
431        available_modules: module_init_registry.kinds(),
432        default_modules: module_init_registry.default_modules(),
433    };
434
435    let db = Database::new(
436        RocksDb::build(server_opts.data_dir.join(DB_FILE))
437            .open()
438            .await
439            .map_err(anyhow::Error::from)
440            .unwrap(),
441        ModuleRegistry::default(),
442    );
443
444    let dyn_server_bitcoin_rpc = match (
445        server_opts.bitcoind_url.as_ref(),
446        server_opts.esplora_url.as_ref(),
447    ) {
448        (Some(_), None) => {
449            let bitcoind_username = server_opts
450                .bitcoind_username
451                .clone()
452                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
453            let (bitcoind_url, bitcoind_password) = server_opts
454                .get_bitcoind_url_and_password()
455                .await
456                .expect("Failed to get bitcoind url");
457            BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
458                .unwrap()
459                .into_dyn()
460        }
461        (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
462        (Some(_), Some(esplora_url)) => {
463            let bitcoind_username = server_opts
464                .bitcoind_username
465                .clone()
466                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
467            let (bitcoind_url, bitcoind_password) = server_opts
468                .get_bitcoind_url_and_password()
469                .await
470                .expect("Failed to get bitcoind url");
471            BitcoindClientWithFallback::new(
472                bitcoind_username,
473                bitcoind_password,
474                &bitcoind_url,
475                esplora_url,
476            )
477            .await
478            .unwrap()
479            .into_dyn()
480        }
481        _ => unreachable!("ArgGroup already enforced at least one Bitcoin backend"),
482    };
483    let dyn_server_bitcoin_rpc =
484        ServerBitcoinRpcTracked::new(dyn_server_bitcoin_rpc, "server").into_dyn();
485
486    root_task_group.install_kill_handler();
487
488    install_crypto_provider().await;
489
490    // The UI password falls back to the legacy password.private file on disk
491    // if its env var is not set, since the UI defaults to a loopback bind.
492    // The API password never falls back: the public admin API is always
493    // network-reachable, so it must be enabled explicitly via its env var or
494    // else admin RPCs return 401. Absence of a password for a given plane opts
495    // that plane into passwordless mode (UI) or disabled admin RPCs (API).
496    let password_file = std::fs::read_to_string(server_opts.data_dir.join(PLAINTEXT_PASSWORD))
497        .ok()
498        .map(|s| s.trim().to_owned());
499
500    let auth_ui = server_opts.password_ui.or(password_file).map(ApiAuth::new);
501    let auth_api = server_opts.password_api.map(ApiAuth::new);
502
503    let task_group = root_task_group.clone();
504    let code_version_hash = code_version_hash.to_string();
505    root_task_group.spawn_cancellable("main", async move {
506        fedimint_server::run_with_iroh_p2p_relays_and_next_api(
507            server_opts.data_dir,
508            auth_ui,
509            auth_api,
510            server_opts.force_api_secrets,
511            settings,
512            db,
513            code_version_str,
514            code_version_hash,
515            module_init_registry,
516            task_group,
517            dyn_server_bitcoin_rpc,
518            Box::new(fedimint_server_ui::setup::router),
519            Box::new(fedimint_server_ui::dashboard::router),
520            server_opts.db_checkpoint_retention,
521            Duration::from_secs(server_opts.session_timeout_secs),
522            server_opts
523                .p2p_max_connection_age_secs
524                .map(Duration::from_secs),
525            fedimint_server::ConnectionLimits::new(
526                server_opts.iroh_api_max_connections,
527                server_opts.iroh_api_max_requests_per_connection,
528            ),
529            server_opts.iroh_p2p_relays,
530            iroh_next_api_settings,
531        )
532        .await
533        .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact()));
534    });
535
536    let shutdown_future = root_task_group
537        .make_handle()
538        .make_shutdown_rx()
539        .then(|()| async {
540            info!(target: LOG_CORE, "Shutdown called");
541        });
542
543    shutdown_future.await;
544
545    debug!(target: LOG_CORE, "Terminating main task");
546
547    if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
548        error!(target: LOG_CORE, err = %err.fmt_compact(), "Error while shutting down task group");
549    }
550
551    debug!(target: LOG_CORE, "Shutdown complete");
552
553    fedimint_logging::shutdown();
554
555    drop(timing_total_runtime);
556
557    std::process::exit(-1);
558}
559
560pub fn default_modules() -> ServerModuleInitRegistry {
561    let mut server_gens = ServerModuleInitRegistry::new();
562
563    server_gens.attach(MintInit);
564    server_gens.attach(fedimint_mintv2_server::MintInit);
565
566    server_gens.attach(WalletInit);
567    server_gens.attach(fedimint_walletv2_server::WalletInit);
568
569    server_gens.attach(LightningInit);
570    server_gens.attach(fedimint_lnv2_server::LightningInit);
571
572    if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
573        server_gens.attach(MetaInit);
574    }
575
576    if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
577        server_gens.attach(UnknownInit);
578    }
579
580    server_gens
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    fn server_opts_args() -> Vec<&'static str> {
588        vec![
589            "fedimintd",
590            "--data-dir",
591            "/tmp/fedimintd-test",
592            "--bitcoind-url",
593            "http://127.0.0.1:18443",
594            "--bitcoind-username",
595            "user",
596            "--bitcoind-password",
597            "pass",
598        ]
599    }
600
601    fn parse_server_opts() -> ServerOpts {
602        ServerOpts::try_parse_from(server_opts_args()).expect("server opts should parse")
603    }
604
605    #[test]
606    fn both_bitcoin_backends_parse_together() {
607        let mut args = server_opts_args();
608        args.extend(["--esplora-url", "http://127.0.0.1:50002"]);
609        let opts = ServerOpts::try_parse_from(args).expect("both Bitcoin backends should parse");
610        assert!(opts.bitcoind_url.is_some());
611        assert!(opts.esplora_url.is_some());
612    }
613
614    fn parse_server_opts_with_enable_iroh_env(value: &str) -> ServerOpts {
615        let previous = std::env::var_os(FM_ENABLE_IROH_ENV);
616        // This test does not spawn threads while mutating the process
617        // environment.
618        unsafe {
619            std::env::set_var(FM_ENABLE_IROH_ENV, value);
620        }
621
622        let opts = parse_server_opts();
623
624        // This test does not spawn threads while mutating the process
625        // environment.
626        unsafe {
627            if let Some(previous) = previous {
628                std::env::set_var(FM_ENABLE_IROH_ENV, previous);
629            } else {
630                std::env::remove_var(FM_ENABLE_IROH_ENV);
631            }
632        }
633
634        opts
635    }
636
637    #[test]
638    fn enable_iroh_env_accepts_numeric_booleans() {
639        assert_eq!(
640            parse_server_opts_with_enable_iroh_env("1").enable_iroh,
641            Some(true)
642        );
643        assert_eq!(
644            parse_server_opts_with_enable_iroh_env("0").enable_iroh,
645            Some(false)
646        );
647    }
648
649    #[test]
650    fn iroh_next_api_defaults_to_enabled_and_accepts_false() {
651        let command = ServerOpts::command();
652        let enable_iroh_next = command
653            .get_arguments()
654            .find(|arg| arg.get_id() == "enable_iroh_next")
655            .expect("enable-iroh-next argument exists");
656        assert_eq!(enable_iroh_next.get_default_values(), ["true"]);
657
658        let mut args = server_opts_args();
659        args.push("--enable-iroh-next=false");
660        let opts = ServerOpts::try_parse_from(args).expect("explicit false should parse");
661        assert!(!opts.enable_iroh_next);
662    }
663
664    #[test]
665    fn p2p_relay_does_not_require_enable_iroh_or_change_api_relays() {
666        let opts = ServerOpts::try_parse_from([
667            "fedimintd",
668            "--data-dir",
669            "/tmp/fedimintd-test",
670            "--bitcoind-url",
671            "http://127.0.0.1:18443",
672            "--bitcoind-username",
673            "user",
674            "--bitcoind-password",
675            "pass",
676            "--iroh-p2p-relays",
677            "https://relay.example.com/",
678        ])
679        .expect("P2P relay should parse independently");
680
681        assert!(opts.iroh_relays.is_empty());
682        assert_eq!(opts.iroh_p2p_relays.len(), 1);
683    }
684}