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