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::module::ApiAuth;
40use fedimint_core::net::DynP2PConnections;
41use fedimint_core::session_outcome::ConsensusItem;
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::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().bind().await;
297
298    Box::pin(consensus::run(
299        connectors,
300        auth_ui,
301        auth_api,
302        connections,
303        p2p_status_receivers,
304        settings.api_bind,
305        settings.iroh_dns,
306        settings.iroh_relays,
307        cfg,
308        db,
309        module_init_registry.clone(),
310        &task_group,
311        force_api_secrets,
312        data_dir,
313        code_version_str,
314        code_version_hash,
315        bitcoin_rpc,
316        settings.ui_bind,
317        dashboard_ui_router,
318        db_checkpoint_retention,
319        session_timeout,
320        iroh_api_limits,
321        iroh_next_api_settings.as_ref(),
322    ))
323    .await?;
324
325    info!(target: LOG_CONSENSUS, "Shutting down tasks...");
326
327    task_group.shutdown();
328
329    Ok(())
330}
331
332async fn update_server_info_version_dbtx(
333    dbtx: &mut DatabaseTransaction<'_>,
334    code_version_str: &str,
335) {
336    let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
337        init_version: code_version_str.to_string(),
338        last_version: code_version_str.to_string(),
339    });
340    server_info.last_version = code_version_str.to_string();
341    dbtx.insert_entry(&ServerInfoKey, &server_info).await;
342}
343
344pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
345    if !data_dir.join("consensus.json").exists() {
346        return Ok(None);
347    }
348
349    read_server_config(data_dir).map(Some)
350}
351
352/// Validate restored TCP transport material before building `TlsTcpConnector`.
353///
354/// `ServerConfig::tls_config()` and `TlsTcpConnector::new()` contain invariant
355/// checks that are fine for generated configs but too panic-prone for uploaded
356/// restore data. This preflights the same key/certificate material and returns
357/// a normal restore error before any files are installed.
358///
359/// Keep this in sync with the panic points in `ServerConfig::tls_config()`:
360/// missing `private.tls_key`, malformed TLS private-key hex, malformed TLS
361/// certificate hex, and malformed TLS private-key DER.
362fn validate_restored_tcp_config(cfg: &ServerConfig) -> anyhow::Result<()> {
363    let tls_key = cfg
364        .private
365        .tls_key
366        .as_ref()
367        .context("Restored TCP config is missing the TLS private key")?;
368    let tls_key_bytes = Vec::from_hex(tls_key).context("Parsing restored TLS private key")?;
369    rustls::pki_types::PrivateKeyDer::try_from(tls_key_bytes)
370        .map_err(|e| anyhow::format_err!("Parsing restored TLS private key DER: {e}"))?;
371
372    ensure!(
373        cfg.consensus.tls_certs.contains_key(&cfg.local.identity),
374        "Restored TCP config is missing our TLS certificate"
375    );
376    for (peer, cert) in &cfg.consensus.tls_certs {
377        Vec::from_hex(cert)
378            .with_context(|| format!("Parsing restored TLS certificate for peer {peer}"))?;
379    }
380
381    let tls_config = cfg.tls_config();
382    let mut root_cert_store = rustls::RootCertStore::empty();
383    for cert in tls_config.certificates.values() {
384        root_cert_store
385            .add(cert.clone())
386            .context("Adding restored TLS certificate to root store")?;
387    }
388    let verifier = rustls::server::WebPkiClientVerifier::builder(root_cert_store.into())
389        .build()
390        .context("Creating restored TLS client verifier")?;
391    let certificate = tls_config
392        .certificates
393        .get(&cfg.local.identity)
394        .context("Restored TCP config is missing our TLS certificate")?
395        .clone();
396    rustls::ServerConfig::builder()
397        .with_client_cert_verifier(verifier)
398        .with_single_cert(vec![certificate], tls_config.private_key.clone_key())
399        .context("Creating restored TLS server config")?;
400
401    Ok(())
402}
403
404/// Validate restored Iroh transport keys and return the p2p key for connector
405/// setup.
406///
407/// Restore data must contain both API and p2p secret keys, and both must match
408/// this guardian's public keys in the restored consensus endpoints before the
409/// config is installed.
410fn restored_iroh_p2p_key(cfg: &ServerConfig) -> anyhow::Result<iroh::SecretKey> {
411    let iroh_p2p_sk = cfg
412        .private
413        .iroh_p2p_sk
414        .clone()
415        .context("Restored Iroh config is missing the Iroh p2p secret key")?;
416    let local_endpoints = cfg
417        .consensus
418        .iroh_endpoints
419        .get(&cfg.local.identity)
420        .context("Restored Iroh config is missing our Iroh endpoints")?;
421    ensure!(
422        iroh_p2p_sk.public() == local_endpoints.p2p_pk,
423        "Restored Iroh p2p secret key does not match our Iroh endpoint"
424    );
425
426    let iroh_api_sk = cfg
427        .private
428        .iroh_api_sk
429        .clone()
430        .context("Restored Iroh config is missing the Iroh api secret key")?;
431    ensure!(
432        iroh_api_sk.public() == local_endpoints.api_pk,
433        "Restored Iroh api secret key does not match our Iroh endpoint"
434    );
435
436    Ok(iroh_p2p_sk)
437}
438
439/// Run config generation without configuring custom Iroh 1.0 relays for
440/// guardian P2P.
441///
442/// Use [`run_config_gen_with_iroh_p2p_relays`] to supply version-specific P2P
443/// relays.
444#[allow(clippy::too_many_arguments)]
445pub async fn run_config_gen(
446    data_dir: PathBuf,
447    settings: ConfigGenSettings,
448    db: Database,
449    task_group: &TaskGroup,
450    code_version_str: String,
451    code_version_hash: String,
452    api_secrets: ApiSecrets,
453    setup_ui_handler: SetupUiRouter,
454    module_init_registry: ServerModuleInitRegistry,
455    auth_ui: Option<ApiAuth>,
456    auth_api: Option<ApiAuth>,
457) -> anyhow::Result<(
458    ServerConfig,
459    DynP2PConnections<P2PMessage>,
460    P2PStatusReceivers,
461)> {
462    run_config_gen_with_iroh_p2p_relays(
463        data_dir,
464        settings,
465        db,
466        task_group,
467        code_version_str,
468        code_version_hash,
469        api_secrets,
470        setup_ui_handler,
471        module_init_registry,
472        auth_ui,
473        auth_api,
474        Vec::new(),
475    )
476    .await
477}
478
479/// Run config generation with a separate Iroh 1.0 relay list for guardian P2P.
480#[allow(clippy::too_many_arguments)]
481pub async fn run_config_gen_with_iroh_p2p_relays(
482    data_dir: PathBuf,
483    settings: ConfigGenSettings,
484    db: Database,
485    task_group: &TaskGroup,
486    code_version_str: String,
487    code_version_hash: String,
488    api_secrets: ApiSecrets,
489    setup_ui_handler: SetupUiRouter,
490    module_init_registry: ServerModuleInitRegistry,
491    auth_ui: Option<ApiAuth>,
492    auth_api: Option<ApiAuth>,
493    iroh_p2p_relays: Vec<SafeUrl>,
494) -> anyhow::Result<(
495    ServerConfig,
496    DynP2PConnections<P2PMessage>,
497    P2PStatusReceivers,
498)> {
499    info!(target: LOG_CONSENSUS, "Starting config gen");
500
501    initialize_gauge_metrics(task_group, &db).await;
502
503    let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
504
505    let setup_api = SetupApi::new(
506        settings.clone(),
507        db.clone(),
508        cgp_sender,
509        code_version_str.clone(),
510        code_version_hash,
511        auth_ui,
512        auth_api,
513    );
514
515    let mut rpc_module = RpcModule::new(setup_api.clone());
516
517    net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
518
519    let api_handler = net::api::spawn(
520        "setup",
521        // config gen always uses ws api
522        settings.api_bind,
523        rpc_module,
524        10,
525        api_secrets.clone(),
526    )
527    .await;
528
529    let ui_task_group = TaskGroup::new();
530
531    let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
532
533    let ui_listener = TcpListener::bind(settings.ui_bind)
534        .await
535        .expect("Failed to bind setup UI");
536
537    ui_task_group.spawn("setup-ui", move |handle| async move {
538        axum::serve(ui_listener, ui_service)
539            .with_graceful_shutdown(handle.make_shutdown_rx())
540            .await
541            .expect("Failed to serve setup UI");
542    });
543
544    info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
545
546    loop {
547        let config_gen_outcome = cgp_receiver
548            .recv()
549            .await
550            .expect("Config gen params receiver closed unexpectedly");
551
552        match config_gen_outcome {
553            ConfigGenOutcome::Generated(cg_params) => {
554                // HACK: The `start-dkg` API call needs to have some time to finish
555                // before we shut down api handling. There's no easy and good way to do
556                // that other than just giving it some grace period.
557                sleep(Duration::from_millis(100)).await;
558
559                api_handler
560                    .stop()
561                    .expect("Config api should still be running");
562
563                api_handler.stopped().await;
564
565                ui_task_group
566                    .shutdown_join_all(None)
567                    .await
568                    .context("Failed to shutdown UI server after config gen")?;
569
570                let cg_params = *cg_params;
571                let connector = if cg_params.iroh_endpoints().is_empty() {
572                    TlsTcpConnector::new(
573                        cg_params.tls_config(),
574                        settings.p2p_bind,
575                        cg_params.p2p_urls(),
576                        cg_params.identity,
577                    )
578                    .await
579                    .into_dyn()
580                } else {
581                    IrohConnector::new(
582                        cg_params
583                            .iroh_p2p_sk
584                            .clone()
585                            .expect("Iroh p2p secret key is required for iroh endpoints"),
586                        settings.p2p_bind,
587                        settings.iroh_dns,
588                        iroh_p2p_relays,
589                        cg_params
590                            .iroh_endpoints()
591                            .iter()
592                            .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
593                            .collect(),
594                    )
595                    .await?
596                    .into_dyn()
597                };
598
599                let (p2p_status_senders, p2p_status_receivers) =
600                    p2p_status_channels(connector.peers());
601
602                let connections = ReconnectP2PConnections::new(
603                    cg_params.identity,
604                    connector,
605                    task_group,
606                    p2p_status_senders,
607                    None,
608                )
609                .into_dyn();
610
611                let cfg = ServerConfig::distributed_gen(
612                    &cg_params,
613                    module_init_registry.clone(),
614                    code_version_str.clone(),
615                    connections.clone(),
616                    p2p_status_receivers.clone(),
617                )
618                .await?;
619
620                assert_ne!(
621                    cfg.consensus.iroh_endpoints.is_empty(),
622                    cfg.consensus.api_endpoints.is_empty(),
623                );
624
625                write_server_config(
626                    &cfg,
627                    &data_dir,
628                    &module_init_registry,
629                    api_secrets.get_active(),
630                )?;
631
632                return Ok((cfg, connections, p2p_status_receivers));
633            }
634            ConfigGenOutcome::Restored(restored, restore_result_sender) => {
635                // Process restore outcomes while setup serving is still running. This lets the
636                // HTTP handler wait for a precise success/error acknowledgement and keeps setup
637                // retryable if validation or the config write fails.
638                let result: anyhow::Result<_> = async {
639                    let cfg = *restored;
640
641                    module_init_registry.decoders_strict(
642                        cfg.consensus
643                            .modules
644                            .iter()
645                            .map(|(id, config)| (*id, &config.kind)),
646                    )?;
647
648                    cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
649
650                    if cfg.consensus.iroh_endpoints.is_empty() {
651                        validate_restored_tcp_config(&cfg)?;
652                    }
653
654                    // Build the connector from the already validated restored config before
655                    // writing it to its final location.
656                    let connector = if cfg.consensus.iroh_endpoints.is_empty() {
657                        TlsTcpConnector::new(
658                            cfg.tls_config(),
659                            settings.p2p_bind,
660                            cfg.local.p2p_endpoints.clone(),
661                            cfg.local.identity,
662                        )
663                        .await
664                        .into_dyn()
665                    } else {
666                        let iroh_p2p_sk = restored_iroh_p2p_key(&cfg)?;
667
668                        IrohConnector::new(
669                            iroh_p2p_sk,
670                            settings.p2p_bind,
671                            settings.iroh_dns.clone(),
672                            iroh_p2p_relays.clone(),
673                            cfg.consensus
674                                .iroh_endpoints
675                                .iter()
676                                .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
677                                .collect(),
678                        )
679                        .await?
680                        .into_dyn()
681                    };
682
683                    let (p2p_status_senders, p2p_status_receivers) =
684                        p2p_status_channels(connector.peers());
685
686                    // Write the restored config directly into the data directory, exactly
687                    // like a freshly generated config.
688                    write_server_config(
689                        &cfg,
690                        &data_dir,
691                        &module_init_registry,
692                        api_secrets.get_active(),
693                    )?;
694
695                    Ok((cfg, connector, p2p_status_senders, p2p_status_receivers))
696                }
697                .await;
698
699                let ack = result
700                    .as_ref()
701                    .map(|_| ())
702                    .map_err(std::string::ToString::to_string);
703                let restore_failed = ack.is_err();
704                let _ = restore_result_sender.send(ack);
705
706                if restore_failed {
707                    continue;
708                }
709
710                // Give the restore API call a chance to return the acknowledged outcome before
711                // shutting down setup serving.
712                sleep(Duration::from_millis(100)).await;
713
714                api_handler
715                    .stop()
716                    .expect("Config api should still be running");
717
718                api_handler.stopped().await;
719
720                ui_task_group
721                    .shutdown_join_all(None)
722                    .await
723                    .context("Failed to shutdown UI server after restored config install")?;
724
725                let (cfg, connector, p2p_status_senders, p2p_status_receivers) = result?;
726                let connections = ReconnectP2PConnections::new(
727                    cfg.local.identity,
728                    connector,
729                    task_group,
730                    p2p_status_senders,
731                    None,
732                )
733                .into_dyn();
734
735                return Ok((cfg, connections, p2p_status_receivers));
736            }
737        }
738    }
739}