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
9pub mod envs;
10mod metrics;
11
12use std::env;
13use std::net::SocketAddr;
14use std::path::PathBuf;
15use std::time::Duration;
16
17use anyhow::Context as _;
18use bitcoin::Network;
19use clap::{ArgGroup, Parser};
20use envs::FM_BITCOIND_URL_PASSWORD_FILE_ENV;
21use fedimint_core::config::{EmptyGenParams, ServerModuleConfigGenParamsRegistry};
22use fedimint_core::db::Database;
23use fedimint_core::envs::{
24 BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
25};
26use fedimint_core::module::registry::ModuleRegistry;
27use fedimint_core::task::TaskGroup;
28use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
29use fedimint_core::{default_esplora_server, timing};
30use fedimint_ln_common::config::{
31 LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
32};
33use fedimint_ln_server::LightningInit;
34use fedimint_logging::{LOG_CORE, TracingSetup};
35use fedimint_meta_server::{MetaGenParams, MetaInit};
36use fedimint_mint_server::MintInit;
37use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
38use fedimint_rocksdb::RocksDb;
39use fedimint_server::config::ConfigGenSettings;
40use fedimint_server::config::io::DB_FILE;
41use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
42use fedimint_server::net::api::ApiSecrets;
43use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
44use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
45use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
46use fedimint_unknown_common::config::UnknownGenParams;
47use fedimint_unknown_server::UnknownInit;
48use fedimint_wallet_server::WalletInit;
49use fedimint_wallet_server::common::config::{
50 WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
51};
52use futures::FutureExt as _;
53use tracing::{debug, error, info};
54
55use crate::envs::{
56 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRCIS_ENV, FM_BIND_P2P_ENV,
57 FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_URL_ENV,
58 FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
59 FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV, FM_P2P_URL_ENV,
60 FM_PORT_ESPLORA_ENV,
61};
62use crate::metrics::APP_START_TS;
63
64const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
66
67#[derive(Parser)]
68#[command(version)]
69#[command(
70 group(
71 ArgGroup::new("bitcoin_rpc")
72 .required(true)
73 .multiple(false)
74 .args(["bitcoind_url", "esplora_url"])
75 )
76)]
77struct ServerOpts {
78 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
80 data_dir: PathBuf,
81
82 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
84 bitcoin_network: Network,
85
86 #[arg(long, env = FM_BITCOIND_URL_ENV)]
88 bitcoind_url: Option<SafeUrl>,
89
90 #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
98 bitcoind_url_password_file: Option<PathBuf>,
99
100 #[arg(long, env = FM_ESPLORA_URL_ENV)]
102 esplora_url: Option<SafeUrl>,
103
104 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
109 bind_p2p: SocketAddr,
110
111 #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
116 bind_api: SocketAddr,
117
118 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
124 bind_ui: SocketAddr,
125
126 #[arg(long, env = FM_P2P_URL_ENV)]
132 p2p_url: Option<SafeUrl>,
133
134 #[arg(long, env = FM_API_URL_ENV)]
140 api_url: Option<SafeUrl>,
141
142 #[arg(long, env = FM_ENABLE_IROH_ENV)]
143 enable_iroh: bool,
144
145 #[arg(long, env = "FM_IROH_DNS", requires = "enable_iroh")]
147 iroh_dns: Option<SafeUrl>,
148
149 #[arg(long, env = "FM_IROH_RELAY", requires = "enable_iroh")]
151 iroh_relays: Vec<SafeUrl>,
152
153 #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
155 db_checkpoint_retention: u64,
156
157 #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
159 bind_tokio_console: Option<SocketAddr>,
160
161 #[arg(long, default_value = "false")]
163 with_jaeger: bool,
164
165 #[arg(long, env = FM_BIND_METRCIS_ENV)]
167 bind_metrics: Option<SocketAddr>,
168
169 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
184 force_api_secrets: ApiSecrets,
185}
186
187impl ServerOpts {
188 pub async fn get_bitcoind_url(&self) -> anyhow::Result<SafeUrl> {
189 let mut url = self
190 .bitcoind_url
191 .clone()
192 .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
193 if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
194 let password = tokio::fs::read_to_string(password_file)
195 .await
196 .context("Failed to read the password")?
197 .trim()
198 .to_owned();
199 url.set_password(Some(&password))
200 .ok()
201 .ok_or_else(|| anyhow::anyhow!("Failed to set the password from the url"))?;
202 }
203
204 Ok(url)
205 }
206}
207
208pub async fn run(
225 modules_fn: fn(
226 Network,
227 ) -> (
228 ServerModuleInitRegistry,
229 ServerModuleConfigGenParamsRegistry,
230 ),
231 code_version_hash: &str,
232 code_version_vendor_suffix: Option<&str>,
233) -> ! {
234 assert_eq!(
235 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
236 code_version_hash.len(),
237 "version_hash must have an expected length"
238 );
239
240 handle_version_hash_command(code_version_hash);
241
242 let fedimint_version = env!("CARGO_PKG_VERSION");
243
244 APP_START_TS
245 .with_label_values(&[fedimint_version, code_version_hash])
246 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
247
248 let server_opts = ServerOpts::parse();
249
250 let mut tracing_builder = TracingSetup::default();
251
252 tracing_builder
253 .tokio_console_bind(server_opts.bind_tokio_console)
254 .with_jaeger(server_opts.with_jaeger);
255
256 tracing_builder.init().unwrap();
257
258 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
259
260 let code_version_str = code_version_vendor_suffix.map_or_else(
261 || fedimint_version.to_string(),
262 |suffix| format!("{fedimint_version}+{suffix}"),
263 );
264
265 let (server_gens, server_gen_params) = modules_fn(server_opts.bitcoin_network);
266
267 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
268
269 let root_task_group = TaskGroup::new();
270
271 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
272 root_task_group.spawn_cancellable(
273 "metrics-server",
274 fedimint_metrics::run_api_server(*bind_metrics, root_task_group.clone()),
275 );
276 }
277
278 let settings = ConfigGenSettings {
279 p2p_bind: server_opts.bind_p2p,
280 api_bind: server_opts.bind_api,
281 ui_bind: server_opts.bind_ui,
282 p2p_url: server_opts.p2p_url.clone(),
283 api_url: server_opts.api_url.clone(),
284 enable_iroh: server_opts.enable_iroh,
285 iroh_dns: server_opts.iroh_dns.clone(),
286 iroh_relays: server_opts.iroh_relays.clone(),
287 modules: server_gen_params.clone(),
288 registry: server_gens.clone(),
289 };
290
291 let db = Database::new(
292 RocksDb::open(server_opts.data_dir.join(DB_FILE))
293 .await
294 .unwrap(),
295 ModuleRegistry::default(),
296 );
297
298 let dyn_server_bitcoin_rpc = match (
299 server_opts.bitcoind_url.as_ref(),
300 server_opts.esplora_url.as_ref(),
301 ) {
302 (Some(_), None) => BitcoindClient::new(
303 &server_opts
304 .get_bitcoind_url()
305 .await
306 .expect("Failed to get bitcoind url"),
307 )
308 .unwrap()
309 .into_dyn(),
310 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
311 _ => unreachable!("ArgGroup already enforced XOR relation"),
312 };
313
314 root_task_group.install_kill_handler();
315
316 let task_group = root_task_group.clone();
317 root_task_group.spawn_cancellable("main", async move {
318 fedimint_server::run(
319 server_opts.data_dir,
320 server_opts.force_api_secrets,
321 settings,
322 db,
323 code_version_str,
324 server_gens,
325 task_group,
326 dyn_server_bitcoin_rpc,
327 Box::new(fedimint_server_ui::setup::router),
328 Box::new(fedimint_server_ui::dashboard::router),
329 server_opts.db_checkpoint_retention,
330 )
331 .await
332 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
333 });
334
335 let shutdown_future = root_task_group
336 .make_handle()
337 .make_shutdown_rx()
338 .then(|()| async {
339 info!(target: LOG_CORE, "Shutdown called");
340 });
341
342 shutdown_future.await;
343 debug!(target: LOG_CORE, "Terminating main task");
344
345 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
346 error!(target: LOG_CORE, ?err, "Error while shutting down task group");
347 }
348
349 debug!(target: LOG_CORE, "Shutdown complete");
350
351 fedimint_logging::shutdown();
352
353 drop(timing_total_runtime);
354
355 std::process::exit(-1);
357}
358
359pub fn default_modules(
360 network: Network,
361) -> (
362 ServerModuleInitRegistry,
363 ServerModuleConfigGenParamsRegistry,
364) {
365 let mut server_gens = ServerModuleInitRegistry::new();
366 let mut server_gen_params = ServerModuleConfigGenParamsRegistry::default();
367
368 let bitcoin_rpc_config = BitcoinRpcConfig {
369 kind: "bitcoind".to_string(),
370 url: "http://unused_dummy.xyz".parse().unwrap(),
371 };
372
373 server_gens.attach(LightningInit);
374 server_gen_params.attach_config_gen_params(
375 LightningInit::kind(),
376 LightningGenParams {
377 local: LightningGenParamsLocal {
378 bitcoin_rpc: bitcoin_rpc_config.clone(),
379 },
380 consensus: LightningGenParamsConsensus { network },
381 },
382 );
383
384 server_gens.attach(MintInit);
385 server_gen_params.attach_config_gen_params(
386 MintInit::kind(),
387 MintGenParams {
388 local: EmptyGenParams::default(),
389 consensus: MintGenParamsConsensus::new(
390 2,
391 fedimint_mint_common::config::FeeConsensus::zero(),
394 ),
395 },
396 );
397
398 server_gens.attach(WalletInit);
399 server_gen_params.attach_config_gen_params(
400 WalletInit::kind(),
401 WalletGenParams {
402 local: WalletGenParamsLocal {
403 bitcoin_rpc: bitcoin_rpc_config.clone(),
404 },
405 consensus: WalletGenParamsConsensus {
406 network,
407 finality_delay: default_finality_delay(network),
408 client_default_bitcoin_rpc: default_esplora_server(
409 network,
410 std::env::var(FM_PORT_ESPLORA_ENV).ok(),
411 ),
412 fee_consensus: fedimint_wallet_server::common::config::FeeConsensus::default(),
413 },
414 },
415 );
416
417 let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
418 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
419
420 if enable_lnv2 {
421 server_gens.attach(fedimint_lnv2_server::LightningInit);
422 server_gen_params.attach_config_gen_params(
423 fedimint_lnv2_server::LightningInit::kind(),
424 fedimint_lnv2_common::config::LightningGenParams {
425 local: fedimint_lnv2_common::config::LightningGenParamsLocal {
426 bitcoin_rpc: bitcoin_rpc_config.clone(),
427 },
428 consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
429 fee_consensus: fedimint_lnv2_common::config::FeeConsensus::new(100).unwrap(),
431 network,
432 },
433 },
434 );
435 }
436
437 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
438 server_gens.attach(MetaInit);
439 server_gen_params.attach_config_gen_params(MetaInit::kind(), MetaGenParams::default());
440 }
441
442 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
443 server_gens.attach(UnknownInit);
444 server_gen_params
445 .attach_config_gen_params(UnknownInit::kind(), UnknownGenParams::default());
446 }
447
448 (server_gens, server_gen_params)
449}
450
451fn default_finality_delay(network: Network) -> u32 {
454 match network {
455 Network::Bitcoin | Network::Regtest => 10,
456 Network::Testnet | Network::Signet | Network::Testnet4 => 2,
457 _ => panic!("Unsupported network"),
458 }
459}