Skip to main content

fedimint_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::cast_sign_loss)]
6#![allow(clippy::doc_markdown)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::missing_panics_doc)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::needless_lifetimes)]
12#![allow(clippy::ref_option)]
13#![allow(clippy::return_self_not_must_use)]
14#![allow(clippy::similar_names)]
15#![allow(clippy::too_many_lines)]
16#![allow(clippy::needless_pass_by_value)]
17#![allow(clippy::manual_let_else)]
18#![allow(clippy::match_wildcard_for_single_variants)]
19#![allow(clippy::trivially_copy_pass_by_ref)]
20
21//! Server side fedimint module traits
22
23extern crate fedimint_core;
24pub mod connection_limits;
25pub mod db;
26
27use std::net::SocketAddr;
28use std::path::{Path, PathBuf};
29use std::time::Duration;
30
31use anyhow::{Context, ensure};
32use bitcoin::hashes::hex::FromHex as _;
33use config::ServerConfig;
34use config::io::read_server_config;
35pub use connection_limits::ConnectionLimits;
36use fedimint_connectors::ConnectorRegistry;
37use fedimint_core::config::P2PMessage;
38use fedimint_core::db::{Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _};
39use fedimint_core::epoch::ConsensusItem;
40use fedimint_core::module::ApiAuth;
41use fedimint_core::net::peers::DynP2PConnections;
42use fedimint_core::task::{TaskGroup, sleep};
43use fedimint_core::util::SafeUrl;
44use fedimint_logging::LOG_CONSENSUS;
45pub use fedimint_server_core as core;
46use fedimint_server_core::ServerModuleInitRegistry;
47use fedimint_server_core::bitcoin_rpc::DynServerBitcoinRpc;
48use fedimint_server_core::dashboard_ui::DynDashboardApi;
49use fedimint_server_core::setup_ui::{DynSetupApi, ISetupApi};
50use jsonrpsee::RpcModule;
51use net::api::ApiSecrets;
52use net::p2p::P2PStatusReceivers;
53use net::p2p_connector::IrohConnector;
54use tokio::net::TcpListener;
55use tokio_rustls::rustls;
56use tracing::info;
57
58use crate::config::ConfigGenSettings;
59use crate::config::io::write_server_config;
60use crate::config::setup::{ConfigGenOutcome, SetupApi};
61use crate::db::{ServerInfo, ServerInfoKey};
62use crate::fedimint_core::net::peers::IP2PConnections;
63use crate::metrics::initialize_gauge_metrics;
64use crate::net::api::announcement::start_api_announcement_service;
65use crate::net::api::pkarr_publish::start_pkarr_publish_service;
66use crate::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
67use crate::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
68
69pub mod metrics;
70
71/// The actual implementation of consensus
72pub mod consensus;
73
74/// Networking for mint-to-mint and client-to-mint communiccation
75pub mod net;
76
77/// Fedimint toplevel config
78pub mod config;
79
80/// Requested settings for the transitional Iroh 1.0 API listener.
81#[derive(Debug, Clone)]
82pub struct IrohNextApiSettings {
83    /// Optional explicit socket address on which to bind the Iroh 1.0 API
84    /// endpoint.
85    bind: Option<SocketAddr>,
86}
87
88impl IrohNextApiSettings {
89    /// Request the transitional listener, optionally overriding its bind
90    /// address.
91    pub fn new(bind: Option<SocketAddr>) -> Self {
92        Self { bind }
93    }
94
95    pub(crate) fn bind_override(&self) -> Option<SocketAddr> {
96        self.bind
97    }
98}
99
100/// A function/closure type for handling dashboard UI
101pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
102
103/// A function/closure type for handling setup UI
104pub type SetupUiRouter = Box<dyn Fn(DynSetupApi) -> axum::Router + Send>;
105
106/// Run a server without configuring custom Iroh 1.0 relays for guardian P2P.
107///
108/// Use [`run_with_iroh_p2p_relays`] to supply version-specific P2P relays.
109#[allow(clippy::too_many_arguments)]
110pub async fn run(
111    data_dir: PathBuf,
112    auth_ui: Option<ApiAuth>,
113    auth_api: Option<ApiAuth>,
114    force_api_secrets: ApiSecrets,
115    settings: ConfigGenSettings,
116    db: Database,
117    code_version_str: String,
118    code_version_hash: String,
119    module_init_registry: ServerModuleInitRegistry,
120    task_group: TaskGroup,
121    bitcoin_rpc: DynServerBitcoinRpc,
122    setup_ui_router: SetupUiRouter,
123    dashboard_ui_router: DashboardUiRouter,
124    db_checkpoint_retention: u64,
125    session_timeout: Duration,
126    p2p_max_connection_age: Option<Duration>,
127    iroh_api_limits: ConnectionLimits,
128) -> anyhow::Result<()> {
129    run_with_iroh_p2p_relays(
130        data_dir,
131        auth_ui,
132        auth_api,
133        force_api_secrets,
134        settings,
135        db,
136        code_version_str,
137        code_version_hash,
138        module_init_registry,
139        task_group,
140        bitcoin_rpc,
141        setup_ui_router,
142        dashboard_ui_router,
143        db_checkpoint_retention,
144        session_timeout,
145        p2p_max_connection_age,
146        iroh_api_limits,
147        Vec::new(),
148    )
149    .await
150}
151
152/// Run a server with a separate Iroh 1.0 relay list for guardian P2P.
153#[allow(clippy::too_many_arguments)]
154pub async fn run_with_iroh_p2p_relays(
155    data_dir: PathBuf,
156    auth_ui: Option<ApiAuth>,
157    auth_api: Option<ApiAuth>,
158    force_api_secrets: ApiSecrets,
159    settings: ConfigGenSettings,
160    db: Database,
161    code_version_str: String,
162    code_version_hash: String,
163    module_init_registry: ServerModuleInitRegistry,
164    task_group: TaskGroup,
165    bitcoin_rpc: DynServerBitcoinRpc,
166    setup_ui_router: SetupUiRouter,
167    dashboard_ui_router: DashboardUiRouter,
168    db_checkpoint_retention: u64,
169    session_timeout: Duration,
170    p2p_max_connection_age: Option<Duration>,
171    iroh_api_limits: ConnectionLimits,
172    iroh_p2p_relays: Vec<SafeUrl>,
173) -> anyhow::Result<()> {
174    run_with_iroh_p2p_relays_and_next_api(
175        data_dir,
176        auth_ui,
177        auth_api,
178        force_api_secrets,
179        settings,
180        db,
181        code_version_str,
182        code_version_hash,
183        module_init_registry,
184        task_group,
185        bitcoin_rpc,
186        setup_ui_router,
187        dashboard_ui_router,
188        db_checkpoint_retention,
189        session_timeout,
190        p2p_max_connection_age,
191        iroh_api_limits,
192        iroh_p2p_relays,
193        None,
194    )
195    .await
196}
197
198/// Run a server with explicit guardian P2P relays and an optional Iroh 1.0 API.
199#[allow(clippy::too_many_arguments)]
200pub async fn run_with_iroh_p2p_relays_and_next_api(
201    data_dir: PathBuf,
202    auth_ui: Option<ApiAuth>,
203    auth_api: Option<ApiAuth>,
204    force_api_secrets: ApiSecrets,
205    settings: ConfigGenSettings,
206    db: Database,
207    code_version_str: String,
208    code_version_hash: String,
209    module_init_registry: ServerModuleInitRegistry,
210    task_group: TaskGroup,
211    bitcoin_rpc: DynServerBitcoinRpc,
212    setup_ui_router: SetupUiRouter,
213    dashboard_ui_router: DashboardUiRouter,
214    db_checkpoint_retention: u64,
215    session_timeout: Duration,
216    p2p_max_connection_age: Option<Duration>,
217    iroh_api_limits: ConnectionLimits,
218    iroh_p2p_relays: Vec<SafeUrl>,
219    iroh_next_api_settings: Option<IrohNextApiSettings>,
220) -> anyhow::Result<()> {
221    let (cfg, connections, p2p_status_receivers) = match get_config(&data_dir)? {
222        Some(cfg) => {
223            let connector = if cfg.consensus.iroh_endpoints.is_empty() {
224                TlsTcpConnector::new(
225                    cfg.tls_config(),
226                    settings.p2p_bind,
227                    cfg.local.p2p_endpoints.clone(),
228                    cfg.local.identity,
229                )
230                .await
231                .into_dyn()
232            } else {
233                IrohConnector::new(
234                    cfg.private.iroh_p2p_sk.clone().unwrap(),
235                    settings.p2p_bind,
236                    settings.iroh_dns.clone(),
237                    iroh_p2p_relays.clone(),
238                    cfg.consensus
239                        .iroh_endpoints
240                        .iter()
241                        .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
242                        .collect(),
243                )
244                .await?
245                .into_dyn()
246            };
247
248            let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
249
250            let connections = ReconnectP2PConnections::new(
251                cfg.local.identity,
252                connector,
253                &task_group,
254                p2p_status_senders,
255                p2p_max_connection_age,
256            )
257            .into_dyn();
258
259            (cfg, connections, p2p_status_receivers)
260        }
261        None => {
262            Box::pin(run_config_gen_with_iroh_p2p_relays(
263                data_dir.clone(),
264                settings.clone(),
265                db.clone(),
266                &task_group,
267                code_version_str.clone(),
268                code_version_hash.clone(),
269                force_api_secrets.clone(),
270                setup_ui_router,
271                module_init_registry.clone(),
272                auth_ui.clone(),
273                auth_api.clone(),
274                iroh_p2p_relays,
275            ))
276            .await?
277        }
278    };
279
280    let decoders = module_init_registry.decoders_strict(
281        cfg.consensus
282            .modules
283            .iter()
284            .map(|(id, config)| (*id, &config.kind)),
285    )?;
286
287    let db = db.with_decoders(decoders);
288
289    initialize_gauge_metrics(&task_group, &db).await;
290
291    start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
292    start_pkarr_publish_service(&db, &task_group, &cfg).await?;
293
294    info!(target: LOG_CONSENSUS, "Starting consensus...");
295
296    let connectors = ConnectorRegistry::build_from_server_defaults()
297        .bind()
298        .await?;
299
300    Box::pin(consensus::run(
301        connectors,
302        auth_ui,
303        auth_api,
304        connections,
305        p2p_status_receivers,
306        settings.api_bind,
307        settings.iroh_dns,
308        settings.iroh_relays,
309        cfg,
310        db,
311        module_init_registry.clone(),
312        &task_group,
313        force_api_secrets,
314        data_dir,
315        code_version_str,
316        code_version_hash,
317        bitcoin_rpc,
318        settings.ui_bind,
319        dashboard_ui_router,
320        db_checkpoint_retention,
321        session_timeout,
322        iroh_api_limits,
323        iroh_next_api_settings.as_ref(),
324    ))
325    .await?;
326
327    info!(target: LOG_CONSENSUS, "Shutting down tasks...");
328
329    task_group.shutdown();
330
331    Ok(())
332}
333
334async fn update_server_info_version_dbtx(
335    dbtx: &mut DatabaseTransaction<'_>,
336    code_version_str: &str,
337) {
338    let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
339        init_version: code_version_str.to_string(),
340        last_version: code_version_str.to_string(),
341    });
342    server_info.last_version = code_version_str.to_string();
343    dbtx.insert_entry(&ServerInfoKey, &server_info).await;
344}
345
346pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
347    if !data_dir.join("consensus.json").exists() {
348        return Ok(None);
349    }
350
351    read_server_config(data_dir).map(Some)
352}
353
354/// Validate restored TCP transport material before building `TlsTcpConnector`.
355///
356/// `ServerConfig::tls_config()` and `TlsTcpConnector::new()` contain invariant
357/// checks that are fine for generated configs but too panic-prone for uploaded
358/// restore data. This preflights the same key/certificate material and returns
359/// a normal restore error before any files are installed.
360///
361/// Keep this in sync with the panic points in `ServerConfig::tls_config()`:
362/// missing `private.tls_key`, malformed TLS private-key hex, malformed TLS
363/// certificate hex, and malformed TLS private-key DER.
364fn validate_restored_tcp_config(cfg: &ServerConfig) -> anyhow::Result<()> {
365    let tls_key = cfg
366        .private
367        .tls_key
368        .as_ref()
369        .context("Restored TCP config is missing the TLS private key")?;
370    let tls_key_bytes = Vec::from_hex(tls_key).context("Parsing restored TLS private key")?;
371    rustls::pki_types::PrivateKeyDer::try_from(tls_key_bytes)
372        .map_err(|e| anyhow::format_err!("Parsing restored TLS private key DER: {e}"))?;
373
374    ensure!(
375        cfg.consensus.tls_certs.contains_key(&cfg.local.identity),
376        "Restored TCP config is missing our TLS certificate"
377    );
378    for (peer, cert) in &cfg.consensus.tls_certs {
379        Vec::from_hex(cert)
380            .with_context(|| format!("Parsing restored TLS certificate for peer {peer}"))?;
381    }
382
383    let tls_config = cfg.tls_config();
384    let mut root_cert_store = rustls::RootCertStore::empty();
385    for cert in tls_config.certificates.values() {
386        root_cert_store
387            .add(cert.clone())
388            .context("Adding restored TLS certificate to root store")?;
389    }
390    let verifier = rustls::server::WebPkiClientVerifier::builder(root_cert_store.into())
391        .build()
392        .context("Creating restored TLS client verifier")?;
393    let certificate = tls_config
394        .certificates
395        .get(&cfg.local.identity)
396        .context("Restored TCP config is missing our TLS certificate")?
397        .clone();
398    rustls::ServerConfig::builder()
399        .with_client_cert_verifier(verifier)
400        .with_single_cert(vec![certificate], tls_config.private_key.clone_key())
401        .context("Creating restored TLS server config")?;
402
403    Ok(())
404}
405
406/// Validate restored Iroh transport keys and return the p2p key for connector
407/// setup.
408///
409/// Restore data must contain both API and p2p secret keys, and both must match
410/// this guardian's public keys in the restored consensus endpoints before the
411/// config is installed.
412fn restored_iroh_p2p_key(cfg: &ServerConfig) -> anyhow::Result<iroh::SecretKey> {
413    let iroh_p2p_sk = cfg
414        .private
415        .iroh_p2p_sk
416        .clone()
417        .context("Restored Iroh config is missing the Iroh p2p secret key")?;
418    let local_endpoints = cfg
419        .consensus
420        .iroh_endpoints
421        .get(&cfg.local.identity)
422        .context("Restored Iroh config is missing our Iroh endpoints")?;
423    ensure!(
424        iroh_p2p_sk.public() == local_endpoints.p2p_pk,
425        "Restored Iroh p2p secret key does not match our Iroh endpoint"
426    );
427
428    let iroh_api_sk = cfg
429        .private
430        .iroh_api_sk
431        .clone()
432        .context("Restored Iroh config is missing the Iroh api secret key")?;
433    ensure!(
434        iroh_api_sk.public() == local_endpoints.api_pk,
435        "Restored Iroh api secret key does not match our Iroh endpoint"
436    );
437
438    Ok(iroh_p2p_sk)
439}
440
441/// Run config generation without configuring custom Iroh 1.0 relays for
442/// guardian P2P.
443///
444/// Use [`run_config_gen_with_iroh_p2p_relays`] to supply version-specific P2P
445/// relays.
446#[allow(clippy::too_many_arguments)]
447pub async fn run_config_gen(
448    data_dir: PathBuf,
449    settings: ConfigGenSettings,
450    db: Database,
451    task_group: &TaskGroup,
452    code_version_str: String,
453    code_version_hash: String,
454    api_secrets: ApiSecrets,
455    setup_ui_handler: SetupUiRouter,
456    module_init_registry: ServerModuleInitRegistry,
457    auth_ui: Option<ApiAuth>,
458    auth_api: Option<ApiAuth>,
459) -> anyhow::Result<(
460    ServerConfig,
461    DynP2PConnections<P2PMessage>,
462    P2PStatusReceivers,
463)> {
464    run_config_gen_with_iroh_p2p_relays(
465        data_dir,
466        settings,
467        db,
468        task_group,
469        code_version_str,
470        code_version_hash,
471        api_secrets,
472        setup_ui_handler,
473        module_init_registry,
474        auth_ui,
475        auth_api,
476        Vec::new(),
477    )
478    .await
479}
480
481/// Run config generation with a separate Iroh 1.0 relay list for guardian P2P.
482#[allow(clippy::too_many_arguments)]
483pub async fn run_config_gen_with_iroh_p2p_relays(
484    data_dir: PathBuf,
485    settings: ConfigGenSettings,
486    db: Database,
487    task_group: &TaskGroup,
488    code_version_str: String,
489    code_version_hash: String,
490    api_secrets: ApiSecrets,
491    setup_ui_handler: SetupUiRouter,
492    module_init_registry: ServerModuleInitRegistry,
493    auth_ui: Option<ApiAuth>,
494    auth_api: Option<ApiAuth>,
495    iroh_p2p_relays: Vec<SafeUrl>,
496) -> anyhow::Result<(
497    ServerConfig,
498    DynP2PConnections<P2PMessage>,
499    P2PStatusReceivers,
500)> {
501    info!(target: LOG_CONSENSUS, "Starting config gen");
502
503    initialize_gauge_metrics(task_group, &db).await;
504
505    let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
506
507    let setup_api = SetupApi::new(
508        settings.clone(),
509        db.clone(),
510        cgp_sender,
511        code_version_str.clone(),
512        code_version_hash,
513        auth_ui,
514        auth_api,
515    );
516
517    let mut rpc_module = RpcModule::new(setup_api.clone());
518
519    net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
520
521    let api_handler = net::api::spawn(
522        "setup",
523        // config gen always uses ws api
524        settings.api_bind,
525        rpc_module,
526        10,
527        api_secrets.clone(),
528    )
529    .await;
530
531    let ui_task_group = TaskGroup::new();
532
533    let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
534
535    let ui_listener = TcpListener::bind(settings.ui_bind)
536        .await
537        .expect("Failed to bind setup UI");
538
539    ui_task_group.spawn("setup-ui", move |handle| async move {
540        axum::serve(ui_listener, ui_service)
541            .with_graceful_shutdown(handle.make_shutdown_rx())
542            .await
543            .expect("Failed to serve setup UI");
544    });
545
546    info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
547
548    loop {
549        let config_gen_outcome = cgp_receiver
550            .recv()
551            .await
552            .expect("Config gen params receiver closed unexpectedly");
553
554        match config_gen_outcome {
555            ConfigGenOutcome::Generated(cg_params) => {
556                // HACK: The `start-dkg` API call needs to have some time to finish
557                // before we shut down api handling. There's no easy and good way to do
558                // that other than just giving it some grace period.
559                sleep(Duration::from_millis(100)).await;
560
561                api_handler
562                    .stop()
563                    .expect("Config api should still be running");
564
565                api_handler.stopped().await;
566
567                ui_task_group
568                    .shutdown_join_all(None)
569                    .await
570                    .context("Failed to shutdown UI server after config gen")?;
571
572                let cg_params = *cg_params;
573                let connector = if cg_params.iroh_endpoints().is_empty() {
574                    TlsTcpConnector::new(
575                        cg_params.tls_config(),
576                        settings.p2p_bind,
577                        cg_params.p2p_urls(),
578                        cg_params.identity,
579                    )
580                    .await
581                    .into_dyn()
582                } else {
583                    IrohConnector::new(
584                        cg_params
585                            .iroh_p2p_sk
586                            .clone()
587                            .expect("Iroh p2p secret key is required for iroh endpoints"),
588                        settings.p2p_bind,
589                        settings.iroh_dns,
590                        iroh_p2p_relays,
591                        cg_params
592                            .iroh_endpoints()
593                            .iter()
594                            .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
595                            .collect(),
596                    )
597                    .await?
598                    .into_dyn()
599                };
600
601                let (p2p_status_senders, p2p_status_receivers) =
602                    p2p_status_channels(connector.peers());
603
604                let connections = ReconnectP2PConnections::new(
605                    cg_params.identity,
606                    connector,
607                    task_group,
608                    p2p_status_senders,
609                    None,
610                )
611                .into_dyn();
612
613                let cfg = ServerConfig::distributed_gen(
614                    &cg_params,
615                    module_init_registry.clone(),
616                    code_version_str.clone(),
617                    connections.clone(),
618                    p2p_status_receivers.clone(),
619                )
620                .await?;
621
622                assert_ne!(
623                    cfg.consensus.iroh_endpoints.is_empty(),
624                    cfg.consensus.api_endpoints.is_empty(),
625                );
626
627                write_server_config(
628                    &cfg,
629                    &data_dir,
630                    &module_init_registry,
631                    api_secrets.get_active(),
632                )?;
633
634                return Ok((cfg, connections, p2p_status_receivers));
635            }
636            ConfigGenOutcome::Restored(restored, restore_result_sender) => {
637                // Process restore outcomes while setup serving is still running. This lets the
638                // HTTP handler wait for a precise success/error acknowledgement and keeps setup
639                // retryable if validation or the config write fails.
640                let result: anyhow::Result<_> = async {
641                    let cfg = *restored;
642
643                    module_init_registry.decoders_strict(
644                        cfg.consensus
645                            .modules
646                            .iter()
647                            .map(|(id, config)| (*id, &config.kind)),
648                    )?;
649
650                    cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
651
652                    if cfg.consensus.iroh_endpoints.is_empty() {
653                        validate_restored_tcp_config(&cfg)?;
654                    }
655
656                    // Build the connector from the already validated restored config before
657                    // writing it to its final location.
658                    let connector = if cfg.consensus.iroh_endpoints.is_empty() {
659                        TlsTcpConnector::new(
660                            cfg.tls_config(),
661                            settings.p2p_bind,
662                            cfg.local.p2p_endpoints.clone(),
663                            cfg.local.identity,
664                        )
665                        .await
666                        .into_dyn()
667                    } else {
668                        let iroh_p2p_sk = restored_iroh_p2p_key(&cfg)?;
669
670                        IrohConnector::new(
671                            iroh_p2p_sk,
672                            settings.p2p_bind,
673                            settings.iroh_dns.clone(),
674                            iroh_p2p_relays.clone(),
675                            cfg.consensus
676                                .iroh_endpoints
677                                .iter()
678                                .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
679                                .collect(),
680                        )
681                        .await?
682                        .into_dyn()
683                    };
684
685                    let (p2p_status_senders, p2p_status_receivers) =
686                        p2p_status_channels(connector.peers());
687
688                    // Write the restored config directly into the data directory, exactly
689                    // like a freshly generated config.
690                    write_server_config(
691                        &cfg,
692                        &data_dir,
693                        &module_init_registry,
694                        api_secrets.get_active(),
695                    )?;
696
697                    Ok((cfg, connector, p2p_status_senders, p2p_status_receivers))
698                }
699                .await;
700
701                let ack = result
702                    .as_ref()
703                    .map(|_| ())
704                    .map_err(std::string::ToString::to_string);
705                let restore_failed = ack.is_err();
706                let _ = restore_result_sender.send(ack);
707
708                if restore_failed {
709                    continue;
710                }
711
712                // Give the restore API call a chance to return the acknowledged outcome before
713                // shutting down setup serving.
714                sleep(Duration::from_millis(100)).await;
715
716                api_handler
717                    .stop()
718                    .expect("Config api should still be running");
719
720                api_handler.stopped().await;
721
722                ui_task_group
723                    .shutdown_join_all(None)
724                    .await
725                    .context("Failed to shutdown UI server after restored config install")?;
726
727                let (cfg, connector, p2p_status_senders, p2p_status_receivers) = result?;
728                let connections = ReconnectP2PConnections::new(
729                    cfg.local.identity,
730                    connector,
731                    task_group,
732                    p2p_status_senders,
733                    None,
734                )
735                .into_dyn();
736
737                return Ok((cfg, connections, p2p_status_receivers));
738            }
739        }
740    }
741}