1mod metrics;
2
3use std::env;
4use std::net::SocketAddr;
5use std::path::PathBuf;
6use std::time::Duration;
7
8use anyhow::{Context, bail};
9use clap::{Parser, Subcommand};
10use fedimint_core::config::{
11 EmptyGenParams, ModuleInitParams, ServerModuleConfigGenParamsRegistry,
12};
13use fedimint_core::core::ModuleKind;
14use fedimint_core::db::{Database, get_current_database_version};
15use fedimint_core::envs::{
16 BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
17};
18use fedimint_core::module::registry::ModuleRegistry;
19use fedimint_core::module::{ServerApiVersionsSummary, ServerDbVersionsSummary};
20use fedimint_core::task::TaskGroup;
21use fedimint_core::util::{
22 FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command, write_overwrite,
23};
24use fedimint_core::{crit, timing};
25use fedimint_ln_common::config::{
26 LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
27};
28use fedimint_ln_server::LightningInit;
29use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
30use fedimint_meta_server::{MetaGenParams, MetaInit};
31use fedimint_mint_server::MintInit;
32use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
33use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD};
34use fedimint_server::config::{ConfigGenSettings, NetworkingStack, ServerConfig};
35use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
36use fedimint_server::envs::FM_FORCE_IROH_ENV;
37use fedimint_server::net::api::ApiSecrets;
38use fedimint_unknown_common::config::UnknownGenParams;
39use fedimint_unknown_server::UnknownInit;
40use fedimint_wallet_server::WalletInit;
41use fedimint_wallet_server::common::config::{
42 WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
43};
44use futures::FutureExt;
45use tracing::{debug, error, info};
46
47use crate::default_esplora_server;
48use crate::envs::{
49 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_API_IROH_ENV, FM_BIND_API_WS_ENV,
50 FM_BIND_METRICS_API_ENV, FM_BIND_P2P_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV,
51 FM_DATA_DIR_ENV, FM_DISABLE_META_MODULE_ENV, FM_FORCE_API_SECRETS_ENV, FM_P2P_URL_ENV,
52 FM_PASSWORD_ENV, FM_TOKIO_CONSOLE_BIND_ENV,
53};
54use crate::fedimintd::metrics::APP_START_TS;
55
56const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
58
59#[derive(Parser)]
60#[command(version)]
61struct ServerOpts {
62 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
64 data_dir: Option<PathBuf>,
65 #[arg(long, env = FM_PASSWORD_ENV)]
69 password: Option<String>,
70 #[arg(long, env = FM_TOKIO_CONSOLE_BIND_ENV)]
72 tokio_console_bind: Option<SocketAddr>,
73 #[arg(long, default_value = "false")]
75 with_telemetry: bool,
76
77 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
82 bind_p2p: SocketAddr,
83
84 #[arg(long, env = FM_P2P_URL_ENV)]
90 p2p_url: Option<SafeUrl>,
91
92 #[arg(long, env = FM_BIND_API_WS_ENV, default_value = "127.0.0.1:8174")]
100 bind_api_ws: SocketAddr,
101
102 #[arg(long, env = FM_BIND_API_IROH_ENV, default_value = "0.0.0.0:8174" )]
110 bind_api_iroh: SocketAddr,
111
112 #[arg(long, env = FM_API_URL_ENV)]
118 api_url: Option<SafeUrl>,
119
120 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
126 bind_ui: SocketAddr,
127
128 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
130 network: bitcoin::network::Network,
131
132 #[arg(long, env = FM_BIND_METRICS_API_ENV)]
133 bind_metrics_api: Option<SocketAddr>,
134
135 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
150 force_api_secrets: ApiSecrets,
151
152 #[clap(subcommand)]
153 subcommand: Option<ServerSubcommand>,
154}
155
156#[derive(Subcommand)]
157enum ServerSubcommand {
158 #[clap(subcommand)]
160 Dev(DevSubcommand),
161}
162
163#[derive(Subcommand)]
164enum DevSubcommand {
165 ListApiVersions,
167 ListDbVersions,
169}
170
171pub struct Fedimintd {
202 server_gens: ServerModuleInitRegistry,
203 server_gen_params: ServerModuleConfigGenParamsRegistry,
204 code_version_hash: String,
205 code_version_str: String,
206 opts: ServerOpts,
207 bitcoind_rpc: BitcoinRpcConfig,
208}
209
210impl Fedimintd {
211 pub fn new(
226 code_version_hash: &str,
227 code_version_vendor_suffix: Option<&str>,
228 ) -> anyhow::Result<Fedimintd> {
229 assert_eq!(
230 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
231 code_version_hash.len(),
232 "version_hash must have an expected length"
233 );
234
235 handle_version_hash_command(code_version_hash);
236
237 let fedimint_version = env!("CARGO_PKG_VERSION");
238
239 APP_START_TS
240 .with_label_values(&[fedimint_version, code_version_hash])
241 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
242
243 if env::var(FM_BIND_API_ENV).is_ok() && env::var(FM_BIND_API_WS_ENV).is_err() {
246 bail!(
247 "{} environment variable was removed and replaced with two separate: {} and {}. Please unset it.",
248 FM_BIND_API_ENV,
249 FM_BIND_API_WS_ENV,
250 FM_BIND_API_IROH_ENV,
251 );
252 }
253 let opts: ServerOpts = ServerOpts::parse();
254
255 let mut tracing_builder = TracingSetup::default();
256
257 #[cfg(feature = "telemetry")]
258 {
259 tracing_builder
260 .tokio_console_bind(opts.tokio_console_bind)
261 .with_jaeger(opts.with_telemetry);
262 }
263
264 tracing_builder.init().unwrap();
265
266 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
267
268 let bitcoind_rpc = BitcoinRpcConfig::get_defaults_from_env_vars()?;
269
270 Ok(Self {
271 opts,
272 bitcoind_rpc,
273 server_gens: ServerModuleInitRegistry::new(),
274 server_gen_params: ServerModuleConfigGenParamsRegistry::default(),
275 code_version_hash: code_version_hash.to_owned(),
276 code_version_str: code_version_vendor_suffix.map_or_else(
277 || fedimint_version.to_string(),
278 |suffix| format!("{fedimint_version}+{suffix}"),
279 ),
280 })
281 }
282
283 pub fn with_module_kind<T>(mut self, r#gen: T) -> Self
287 where
288 T: ServerModuleInit + 'static + Send + Sync,
289 {
290 self.server_gens.attach(r#gen);
291 self
292 }
293
294 pub fn version_hash(&self) -> &str {
297 &self.code_version_hash
298 }
299
300 pub fn with_module_instance<P>(mut self, kind: ModuleKind, params: P) -> Self
305 where
306 P: ModuleInitParams,
307 {
308 self.server_gen_params
309 .attach_config_gen_params(kind, params);
310 self
311 }
312
313 pub fn with_default_modules(self) -> anyhow::Result<Self> {
315 let network = self.opts.network;
316
317 let bitcoind_rpc = self.bitcoind_rpc.clone();
318 let s = self
319 .with_module_kind(LightningInit)
320 .with_module_instance(
321 LightningInit::kind(),
322 LightningGenParams {
323 local: LightningGenParamsLocal {
324 bitcoin_rpc: bitcoind_rpc.clone(),
325 },
326 consensus: LightningGenParamsConsensus { network },
327 },
328 )
329 .with_module_kind(MintInit)
330 .with_module_instance(
331 MintInit::kind(),
332 MintGenParams {
333 local: EmptyGenParams::default(),
334 consensus: MintGenParamsConsensus::new(
335 2,
336 fedimint_mint_common::config::FeeConsensus::zero(),
339 ),
340 },
341 )
342 .with_module_kind(WalletInit)
343 .with_module_instance(
344 WalletInit::kind(),
345 WalletGenParams {
346 local: WalletGenParamsLocal {
347 bitcoin_rpc: bitcoind_rpc.clone(),
348 },
349 consensus: WalletGenParamsConsensus {
350 network,
351 finality_delay: 10,
352 client_default_bitcoin_rpc: default_esplora_server(network),
353 fee_consensus:
354 fedimint_wallet_server::common::config::FeeConsensus::default(),
355 },
356 },
357 );
358
359 let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
360 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
361
362 let s = if enable_lnv2 {
363 s.with_module_kind(fedimint_lnv2_server::LightningInit)
364 .with_module_instance(
365 fedimint_lnv2_server::LightningInit::kind(),
366 fedimint_lnv2_common::config::LightningGenParams {
367 local: fedimint_lnv2_common::config::LightningGenParamsLocal {
368 bitcoin_rpc: bitcoind_rpc.clone(),
369 },
370 consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
371 fee_consensus: fedimint_lnv2_common::config::FeeConsensus::new(100)?,
373 network,
374 },
375 },
376 )
377 } else {
378 s
379 };
380
381 let s = if is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
382 s
383 } else {
384 s.with_module_kind(MetaInit)
385 .with_module_instance(MetaInit::kind(), MetaGenParams::default())
386 };
387
388 let s = if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
389 s.with_module_kind(UnknownInit)
390 .with_module_instance(UnknownInit::kind(), UnknownGenParams::default())
391 } else {
392 s
393 };
394
395 Ok(s)
396 }
397
398 pub async fn run(self) -> ! {
400 if let Some(subcommand) = &self.opts.subcommand {
402 match subcommand {
403 ServerSubcommand::Dev(DevSubcommand::ListApiVersions) => {
404 let api_versions = self.get_server_api_versions();
405 let api_versions = serde_json::to_string_pretty(&api_versions)
406 .expect("API versions struct is serializable");
407 println!("{api_versions}");
408 std::process::exit(0);
409 }
410 ServerSubcommand::Dev(DevSubcommand::ListDbVersions) => {
411 let db_versions = self.get_server_db_versions();
412 let db_versions = serde_json::to_string_pretty(&db_versions)
413 .expect("API versions struct is serializable");
414 println!("{db_versions}");
415 std::process::exit(0);
416 }
417 }
418 }
419
420 let root_task_group = TaskGroup::new();
421 root_task_group.install_kill_handler();
422
423 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
424
425 let task_group = root_task_group.clone();
426 root_task_group.spawn_cancellable("main", async move {
427 match run(
428 self.opts,
429 &task_group,
430 self.server_gens,
431 self.server_gen_params,
432 self.code_version_str,
433 )
434 .await
435 {
436 Ok(()) => {}
437 Err(error) => {
438 crit!(target: LOG_SERVER, err = %error.fmt_compact_anyhow(), "Main task returned error, shutting down");
439 task_group.shutdown();
440 }
441 }
442 });
443
444 let shutdown_future = root_task_group
445 .make_handle()
446 .make_shutdown_rx()
447 .then(|()| async {
448 info!(target: LOG_CORE, "Shutdown called");
449 });
450
451 shutdown_future.await;
452 debug!(target: LOG_CORE, "Terminating main task");
453
454 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
455 error!(target: LOG_CORE, ?err, "Error while shutting down task group");
456 }
457
458 debug!(target: LOG_CORE, "Shutdown complete");
459
460 fedimint_logging::shutdown();
461
462 drop(timing_total_runtime);
463
464 std::process::exit(-1);
466 }
467
468 fn get_server_api_versions(&self) -> ServerApiVersionsSummary {
469 ServerApiVersionsSummary {
470 core: ServerConfig::supported_api_versions().api,
471 modules: self
472 .server_gens
473 .kinds()
474 .into_iter()
475 .map(|module_kind| {
476 self.server_gens
477 .get(&module_kind)
478 .expect("module is present")
479 })
480 .map(|module_init| {
481 (
482 module_init.module_kind(),
483 module_init.supported_api_versions().api,
484 )
485 })
486 .collect(),
487 }
488 }
489
490 fn get_server_db_versions(&self) -> ServerDbVersionsSummary {
491 ServerDbVersionsSummary {
492 modules: self
493 .server_gens
494 .kinds()
495 .into_iter()
496 .map(|module_kind| {
497 self.server_gens
498 .get(&module_kind)
499 .expect("module is present")
500 })
501 .map(|module_init| {
502 (
503 module_init.module_kind(),
504 get_current_database_version(&module_init.get_database_migrations()),
505 )
506 })
507 .collect(),
508 }
509 }
510}
511
512async fn run(
513 opts: ServerOpts,
514 task_group: &TaskGroup,
515 module_inits: ServerModuleInitRegistry,
516 module_inits_params: ServerModuleConfigGenParamsRegistry,
517 code_version_str: String,
518) -> anyhow::Result<()> {
519 if let Some(socket_addr) = opts.bind_metrics_api.as_ref() {
520 task_group.spawn_cancellable("metrics-server", {
521 let task_group = task_group.clone();
522 let socket_addr = *socket_addr;
523 async move { fedimint_metrics::run_api_server(socket_addr, task_group).await }
524 });
525 }
526
527 let data_dir = opts.data_dir.context("data-dir option is not present")?;
528
529 if let Some(password) = opts.password {
533 write_overwrite(data_dir.join(PLAINTEXT_PASSWORD), password)?;
534 };
535 let use_iroh = is_env_var_set(FM_FORCE_IROH_ENV);
536
537 let settings = ConfigGenSettings {
539 p2p_bind: opts.bind_p2p,
540 bind_api_ws: opts.bind_api_ws,
541 bind_api_iroh: opts.bind_api_iroh,
542 ui_bind: opts.bind_ui,
543 p2p_url: opts.p2p_url,
544 api_url: opts.api_url,
545 modules: module_inits_params.clone(),
546 registry: module_inits.clone(),
547 networking: if use_iroh {
548 NetworkingStack::Iroh
549 } else {
550 NetworkingStack::default()
551 },
552 };
553
554 let db = Database::new(
555 fedimint_rocksdb::RocksDb::open(data_dir.join(DB_FILE)).await?,
556 ModuleRegistry::default(),
557 );
558
559 Box::pin(fedimint_server::run(
560 data_dir,
561 opts.force_api_secrets,
562 settings,
563 db,
564 code_version_str,
565 &module_inits,
566 task_group.clone(),
567 Some(Box::new(fedimint_server_ui::dashboard::start)),
568 Some(Box::new(fedimint_server_ui::setup::start)),
569 ))
570 .await
571}