1use std::collections::{BTreeMap, BTreeSet};
2use std::io::Read as _;
3use std::iter::once;
4use std::mem::discriminant;
5use std::path::{Component, Path, PathBuf};
6use std::str::FromStr as _;
7use std::sync::Arc;
8
9use anyhow::{Context, ensure};
10use async_trait::async_trait;
11use fedimint_core::admin_client::{SetLocalParamsRequest, SetupStatus};
12use fedimint_core::base32::FEDIMINT_PREFIX;
13use fedimint_core::config::META_FEDERATION_NAME_KEY;
14use fedimint_core::core::{ModuleInstanceId, ModuleKind};
15use fedimint_core::db::Database;
16use fedimint_core::endpoint_constants::{
17 ADD_PEER_SETUP_CODE_ENDPOINT, GET_SETUP_CODE_ENDPOINT, RESET_PEER_SETUP_CODES_ENDPOINT,
18 SET_LOCAL_PARAMS_ENDPOINT, SETUP_STATUS_ENDPOINT, START_DKG_ENDPOINT,
19};
20use fedimint_core::envs::{
21 FM_DISABLE_BASE_FEES_ENV, FM_IROH_API_SECRET_KEY_OVERRIDE_ENV,
22 FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV, is_env_var_set,
23};
24use fedimint_core::module::{
25 ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion,
26 admin_api_endpoint, public_api_endpoint,
27};
28use fedimint_core::setup_code::PeerEndpoints;
29use fedimint_core::version::DkgVersion;
30use fedimint_core::{PeerId, base32, runtime};
31use fedimint_server_core::setup_ui::ISetupApi;
32use iroh::SecretKey;
33use rand::rngs::OsRng;
34use tokio::sync::mpsc::Sender;
35use tokio::sync::{Mutex, oneshot};
36use tokio_rustls::rustls;
37
38use crate::config::io::{
39 CONSENSUS_CONFIG, ENCRYPTED_EXT, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG, SALT_FILE,
40 parse_legacy_encrypted_backup, parse_plaintext_backup,
41};
42use crate::config::{ConfigGenParams, ConfigGenSettings, PeerSetupCode, ServerConfig};
43use crate::net::api::HasApiContext;
44use crate::net::p2p_connector::gen_cert_and_key;
45
46pub enum ConfigGenOutcome {
52 Generated(Box<ConfigGenParams>),
53 Restored(Box<ServerConfig>, oneshot::Sender<Result<(), String>>),
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct SetupState {
59 local_params: Option<LocalParams>,
61 setup_codes: BTreeSet<PeerSetupCode>,
63 restore_in_progress: bool,
65}
66
67#[derive(Clone, Debug)]
68pub struct LocalParams {
70 tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
72 iroh_api_sk: Option<iroh::SecretKey>,
74 iroh_p2p_sk: Option<iroh::SecretKey>,
76 endpoints: PeerEndpoints,
78 name: String,
80 federation_name: Option<String>,
82 disable_base_fees: Option<bool>,
84 enabled_modules: Option<BTreeSet<ModuleKind>>,
87 federation_size: Option<u32>,
90 network: bitcoin::Network,
92 fedimint_version: DkgVersion,
94}
95
96impl LocalParams {
97 pub fn setup_code(&self) -> PeerSetupCode {
98 PeerSetupCode {
99 name: self.name.clone(),
100 endpoints: self.endpoints.clone(),
101 federation_name: self.federation_name.clone(),
102 disable_base_fees: self.disable_base_fees,
103 enabled_modules: self.enabled_modules.clone(),
104 federation_size: self.federation_size,
105 network: self.network,
106 fedimint_version: self.fedimint_version.clone(),
107 }
108 }
109}
110
111fn ensure_fedimint_version_matches(
112 peer_setup_code: &PeerSetupCode,
113 local_fedimint_version: &DkgVersion,
114) -> anyhow::Result<()> {
115 ensure!(
116 peer_setup_code.fedimint_version.compatibility_version()
117 == local_fedimint_version.compatibility_version(),
118 "Guardian uses Fedimint version {} but we use {local_fedimint_version}",
119 peer_setup_code.fedimint_version,
120 );
121
122 Ok(())
123}
124
125#[derive(Clone)]
127pub struct SetupApi {
128 settings: ConfigGenSettings,
130 state: Arc<Mutex<SetupState>>,
132 db: Database,
134 sender: Sender<ConfigGenOutcome>,
136 code_version_str: String,
140 code_version_hash: String,
142 auth_ui: Option<ApiAuth>,
144 auth_api: Option<ApiAuth>,
146}
147
148impl SetupApi {
149 pub fn new(
150 settings: ConfigGenSettings,
151 db: Database,
152 sender: Sender<ConfigGenOutcome>,
153 code_version_str: String,
154 code_version_hash: String,
155 auth_ui: Option<ApiAuth>,
156 auth_api: Option<ApiAuth>,
157 ) -> Self {
158 Self {
159 settings,
160 state: Arc::new(Mutex::new(SetupState::default())),
161 db,
162 sender,
163 code_version_str,
164 code_version_hash,
165 auth_ui,
166 auth_api,
167 }
168 }
169
170 pub async fn setup_status(&self) -> SetupStatus {
171 match self.state.lock().await.local_params {
172 Some(..) => SetupStatus::SharingConnectionCodes,
173 None => SetupStatus::AwaitingLocalParams,
174 }
175 }
176}
177
178fn is_expected_backup_path(path: &Path) -> bool {
179 let expected_paths = [
180 PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
181 PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
182 PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
183 PathBuf::from(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT),
184 PathBuf::from(SALT_FILE),
185 ];
186
187 expected_paths.iter().any(|expected| expected == path)
188}
189
190fn parse_backup(backup: &[u8], password: Option<&str>) -> anyhow::Result<ServerConfig> {
200 let mut archive = tar::Archive::new(backup);
201 let mut files: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
202
203 for entry in archive.entries().context("Reading backup archive")? {
204 let mut entry = entry.context("Reading backup archive entry")?;
205 let path = entry
206 .path()
207 .context("Reading backup archive entry path")?
208 .into_owned();
209 ensure!(
210 path.components()
211 .all(|component| matches!(component, Component::Normal(_))),
212 "Backup archive contains an invalid path"
213 );
214 ensure!(
215 is_expected_backup_path(&path),
216 "Backup archive contains unexpected file {}",
217 path.display()
218 );
219 ensure!(
220 entry.header().entry_type().is_file(),
221 "Backup archive contains non-file entry {}",
222 path.display()
223 );
224
225 let mut bytes = Vec::new();
226 entry
227 .read_to_end(&mut bytes)
228 .context("Reading backup archive entry contents")?;
229 ensure!(
230 files.insert(path.clone(), bytes).is_none(),
231 "Backup archive contains duplicate file {}",
232 path.display()
233 );
234 }
235
236 let local_config = PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT);
237 let consensus_config = PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT);
238 let private_config_json = PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT);
239 let private_config_encrypted = PathBuf::from(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT);
240 let salt_file = PathBuf::from(SALT_FILE);
241
242 let local = files
243 .get(&local_config)
244 .with_context(|| format!("Backup archive is missing {}", local_config.display()))?;
245 let consensus = files
246 .get(&consensus_config)
247 .with_context(|| format!("Backup archive is missing {}", consensus_config.display()))?;
248
249 if let Some(private) = files.get(&private_config_json) {
253 parse_plaintext_backup(local, consensus, private).context("Reading restored config")
256 } else if let Some(private) = files.get(&private_config_encrypted) {
257 let salt = files
260 .get(&salt_file)
261 .with_context(|| format!("Backup archive is missing {}", salt_file.display()))?;
262 let password = password.context(
263 "This backup is encrypted, please provide the guardian password used when it was created",
264 )?;
265 parse_legacy_encrypted_backup(local, consensus, private, salt, password)
266 .context("Reading restored config")
267 } else {
268 anyhow::bail!("Backup archive is missing the private config");
269 }
270}
271
272#[async_trait]
273impl ISetupApi for SetupApi {
274 async fn setup_code(&self) -> Option<String> {
275 self.state
276 .lock()
277 .await
278 .local_params
279 .as_ref()
280 .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
281 }
282
283 async fn guardian_name(&self) -> Option<String> {
284 self.state
285 .lock()
286 .await
287 .local_params
288 .as_ref()
289 .map(|lp| lp.name.clone())
290 }
291
292 fn auth_ui(&self) -> Option<ApiAuth> {
293 self.auth_ui.clone()
294 }
295
296 async fn connected_peers(&self) -> Vec<String> {
297 self.state
298 .lock()
299 .await
300 .setup_codes
301 .clone()
302 .into_iter()
303 .map(|info| info.name)
304 .collect()
305 }
306
307 fn available_modules(&self) -> BTreeSet<ModuleKind> {
308 self.settings.available_modules.clone()
309 }
310
311 fn default_modules(&self) -> BTreeSet<ModuleKind> {
312 self.settings.default_modules.clone()
313 }
314
315 async fn reset_setup_codes(&self) {
316 self.state.lock().await.setup_codes.clear();
317 }
318
319 async fn set_local_parameters(
320 &self,
321 name: String,
322 federation_name: Option<String>,
323 disable_base_fees: Option<bool>,
324 enabled_modules: Option<BTreeSet<ModuleKind>>,
325 federation_size: Option<u32>,
326 ) -> anyhow::Result<String> {
327 if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
328 && existing_local_parameters.name == name
329 && existing_local_parameters.federation_name == federation_name
330 && existing_local_parameters.disable_base_fees == disable_base_fees
331 && existing_local_parameters.enabled_modules == enabled_modules
332 && existing_local_parameters.federation_size == federation_size
333 {
334 return Ok(base32::encode_prefixed(
335 FEDIMINT_PREFIX,
336 &existing_local_parameters.setup_code(),
337 ));
338 }
339
340 ensure!(!name.is_empty(), "The guardian name is empty");
341
342 if let Some(federation_name) = federation_name.as_ref() {
343 ensure!(!federation_name.is_empty(), "The federation name is empty");
344 }
345
346 if federation_name.is_some() {
347 ensure!(
348 federation_size.is_some(),
349 "The leader must set the federation size"
350 );
351 }
352
353 if let Some(size) = federation_size {
354 ensure!(
355 size == 1 || 4 <= size,
356 "Federation size must be 1 or at least 4"
357 );
358 }
359
360 let mut state = self.state.lock().await;
361
362 ensure!(
363 state.local_params.is_none(),
364 "Local parameters have already been set"
365 );
366
367 ensure!(
368 !state.restore_in_progress,
369 "A restore is already in progress"
370 );
371
372 let fedimint_version =
373 DkgVersion::parse(&self.code_version_str).context("Invalid local Fedimint version")?;
374
375 let lp = if self.settings.enable_iroh {
376 let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
377 SecretKey::from_str(&var)
378 .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
379 } else {
380 SecretKey::generate(&mut OsRng)
381 };
382
383 let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
384 SecretKey::from_str(&var)
385 .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
386 } else {
387 SecretKey::generate(&mut OsRng)
388 };
389
390 LocalParams {
391 tls_key: None,
392 iroh_api_sk: Some(iroh_api_sk.clone()),
393 iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
394 endpoints: PeerEndpoints::Iroh {
395 api_pk: iroh_api_sk.public(),
396 p2p_pk: iroh_p2p_sk.public(),
397 },
398 name,
399 federation_name,
400 disable_base_fees,
401 enabled_modules,
402 federation_size,
403 network: self.settings.network,
404 fedimint_version: fedimint_version.clone(),
405 }
406 } else {
407 let (tls_cert, tls_key) =
408 gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
409
410 LocalParams {
411 tls_key: Some(tls_key),
412 iroh_api_sk: None,
413 iroh_p2p_sk: None,
414 endpoints: PeerEndpoints::Tcp {
415 api_url: self
416 .settings
417 .api_url
418 .clone()
419 .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
420 p2p_url: self
421 .settings
422 .p2p_url
423 .clone()
424 .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
425
426 cert: tls_cert.as_ref().to_vec(),
427 },
428 name,
429 federation_name,
430 disable_base_fees,
431 enabled_modules,
432 federation_size,
433 network: self.settings.network,
434 fedimint_version,
435 }
436 };
437
438 state.local_params = Some(lp.clone());
439
440 Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
441 }
442
443 async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
444 let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
445
446 let mut state = self.state.lock().await;
447
448 if state.setup_codes.contains(&info) {
449 return Ok(info.name.clone());
450 }
451
452 ensure!(
453 !state.restore_in_progress,
454 "A restore is already in progress"
455 );
456
457 let local_params = state
458 .local_params
459 .clone()
460 .expect("The endpoint is authenticated.");
461
462 ensure!(
463 info != local_params.setup_code(),
464 "You cannot add your own setup code"
465 );
466
467 ensure!(
468 discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
469 "Guardian has different endpoint variant (TCP/Iroh) than us.",
470 );
471
472 ensure_fedimint_version_matches(&info, &local_params.fedimint_version)?;
473
474 ensure!(
475 info.network == local_params.network,
476 "Guardian uses Bitcoin network {} but we use {}",
477 info.network,
478 local_params.network,
479 );
480
481 if let Some(federation_name) = state
482 .setup_codes
483 .iter()
484 .chain(once(&local_params.setup_code()))
485 .find_map(|info| info.federation_name.clone())
486 {
487 ensure!(
488 info.federation_name.is_none(),
489 "Federation name has already been set to {federation_name}"
490 );
491 }
492
493 if let Some(disable_base_fees) = state
494 .setup_codes
495 .iter()
496 .chain(once(&local_params.setup_code()))
497 .find_map(|info| info.disable_base_fees)
498 {
499 ensure!(
500 info.disable_base_fees.is_none(),
501 "Base fees setting has already been configured to disabled={disable_base_fees}"
502 );
503 }
504
505 if state
506 .setup_codes
507 .iter()
508 .chain(once(&local_params.setup_code()))
509 .any(|info| info.enabled_modules.is_some())
510 {
511 ensure!(
512 info.enabled_modules.is_none(),
513 "Enabled modules have already been configured by another guardian"
514 );
515 }
516
517 if let Some(federation_size) = state
518 .setup_codes
519 .iter()
520 .chain(once(&local_params.setup_code()))
521 .find_map(|info| info.federation_size)
522 {
523 ensure!(
524 info.federation_size.is_none(),
525 "Federation size has already been set to {federation_size}"
526 );
527 }
528
529 state.setup_codes.insert(info.clone());
530
531 Ok(info.name)
532 }
533
534 async fn start_dkg(&self) -> anyhow::Result<()> {
535 let mut state = self.state.lock().await.clone();
536
537 ensure!(
538 !state.restore_in_progress,
539 "A restore is already in progress"
540 );
541
542 let local_params = state
543 .local_params
544 .clone()
545 .expect("The endpoint is authenticated.");
546
547 let our_setup_code = local_params.setup_code();
548
549 state.setup_codes.insert(our_setup_code.clone());
550
551 for setup_code in &state.setup_codes {
552 ensure_fedimint_version_matches(setup_code, &local_params.fedimint_version)?;
553 }
554
555 ensure!(
556 state.setup_codes.len() == 1 || 4 <= state.setup_codes.len(),
557 "The number of guardians is invalid"
558 );
559
560 if let Some(federation_size) = state
561 .setup_codes
562 .iter()
563 .find_map(|info| info.federation_size)
564 {
565 ensure!(
566 state.setup_codes.len() == federation_size as usize,
567 "Expected {federation_size} guardians but got {}",
568 state.setup_codes.len()
569 );
570 }
571
572 let federation_name = state
573 .setup_codes
574 .iter()
575 .find_map(|info| info.federation_name.clone())
576 .context("We need one guardian to configure the federations name")?;
577
578 let disable_base_fees = state
579 .setup_codes
580 .iter()
581 .find_map(|info| info.disable_base_fees)
582 .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
583
584 let enabled_modules = state
585 .setup_codes
586 .iter()
587 .find_map(|info| info.enabled_modules.clone())
588 .unwrap_or_else(|| self.settings.default_modules.clone());
589
590 let our_id = state
591 .setup_codes
592 .iter()
593 .position(|info| info == &our_setup_code)
594 .expect("We inserted the key above.");
595
596 let params = ConfigGenParams {
597 identity: PeerId::from(our_id as u16),
598 tls_key: local_params.tls_key,
599 iroh_api_sk: local_params.iroh_api_sk,
600 iroh_p2p_sk: local_params.iroh_p2p_sk,
601 peers: (0..)
602 .map(|i| PeerId::from(i as u16))
603 .zip(state.setup_codes.clone())
604 .collect(),
605 meta: BTreeMap::from_iter(vec![(
606 META_FEDERATION_NAME_KEY.to_string(),
607 federation_name,
608 )]),
609 disable_base_fees,
610 enabled_modules,
611 network: local_params.network,
612 };
613
614 self.sender
615 .send(ConfigGenOutcome::Generated(Box::new(params)))
616 .await
617 .context("Failed to send config gen params")?;
618
619 Ok(())
620 }
621
622 async fn restore_from_backup(
623 &self,
624 password: Option<String>,
625 backup: Vec<u8>,
626 ) -> anyhow::Result<()> {
627 if let Some(password) = &password {
628 ensure!(!password.is_empty(), "The password is empty");
629 ensure!(
630 password.trim() == password,
631 "The password contains leading/trailing whitespace",
632 );
633 }
634 {
635 let mut state = self.state.lock().await;
636 ensure!(
637 state.local_params.is_none(),
638 "Local parameters have already been set"
639 );
640 ensure!(
641 !state.restore_in_progress,
642 "A restore is already in progress"
643 );
644 state.restore_in_progress = true;
645 }
646
647 let state = self.state.clone();
648 let sender = self.sender.clone();
649 runtime::spawn("restore guardian backup", async move {
650 let result = async {
651 let cfg =
652 tokio::task::spawn_blocking(move || parse_backup(&backup, password.as_deref()))
653 .await
654 .context("Restore backup task panicked")??;
655 let (restore_result_sender, restore_result_receiver) = oneshot::channel();
656 let restored = ConfigGenOutcome::Restored(Box::new(cfg), restore_result_sender);
657 if sender.send(restored).await.is_err() {
658 return Err(anyhow::format_err!("Failed to send restored config"));
659 }
660 restore_result_receiver
661 .await
662 .context("Restore result sender dropped")?
663 .map_err(anyhow::Error::msg)?;
664 Ok(())
665 }
666 .await;
667
668 if result.is_err() {
669 state.lock().await.restore_in_progress = false;
670 }
671 result
676 })
677 .await
678 .context("Restore task panicked")?
679 }
680
681 async fn federation_size(&self) -> Option<u32> {
682 let state = self.state.lock().await;
683 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
684 state
685 .setup_codes
686 .iter()
687 .chain(local_setup_code.iter())
688 .find_map(|info| info.federation_size)
689 }
690
691 async fn cfg_federation_name(&self) -> Option<String> {
692 let state = self.state.lock().await;
693 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
694 state
695 .setup_codes
696 .iter()
697 .chain(local_setup_code.iter())
698 .find_map(|info| info.federation_name.clone())
699 }
700
701 async fn cfg_base_fees_disabled(&self) -> Option<bool> {
702 let state = self.state.lock().await;
703 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
704 state
705 .setup_codes
706 .iter()
707 .chain(local_setup_code.iter())
708 .find_map(|info| info.disable_base_fees)
709 }
710
711 async fn cfg_enabled_modules(&self) -> Option<BTreeSet<ModuleKind>> {
712 let state = self.state.lock().await;
713 let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
714 state
715 .setup_codes
716 .iter()
717 .chain(local_setup_code.iter())
718 .find_map(|info| info.enabled_modules.clone())
719 }
720
721 async fn fedimintd_version(&self) -> String {
722 self.code_version_str.clone()
723 }
724
725 async fn fedimintd_version_hash(&self) -> Option<String> {
726 fedimint_core::version::non_zero_version_hash(&self.code_version_hash).map(str::to_owned)
727 }
728}
729
730#[async_trait]
731impl HasApiContext<SetupApi> for SetupApi {
732 async fn context(
733 &self,
734 request: &ApiRequestErased,
735 id: Option<ModuleInstanceId>,
736 ) -> (&SetupApi, ApiEndpointContext) {
737 assert!(id.is_none());
738
739 let db = self.db.clone();
740
741 let is_authenticated = match (&self.auth_api, &request.auth) {
742 (Some(server_auth), Some(req_auth)) => server_auth.verify(req_auth.as_str()),
743 _ => false,
744 };
745
746 let context = ApiEndpointContext::new(db, is_authenticated);
747
748 (self, context)
749 }
750}
751
752pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
753 vec![
754 public_api_endpoint! {
755 SETUP_STATUS_ENDPOINT,
756 ApiVersion::new(0, 0),
757 async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
758 Ok(config.setup_status().await)
759 }
760 },
761 admin_api_endpoint! {
762 SET_LOCAL_PARAMS_ENDPOINT,
763 ApiVersion::new(0, 0),
764 async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
765
766 config.set_local_parameters(request.name, request.federation_name, request.disable_base_fees, request.enabled_modules, request.federation_size)
767 .await
768 .map_err(|e| ApiError::bad_request(e.to_string()))
769 }
770 },
771 admin_api_endpoint! {
772 ADD_PEER_SETUP_CODE_ENDPOINT,
773 ApiVersion::new(0, 0),
774 async |config: &SetupApi, context, info: String| -> String {
775
776 config.add_peer_setup_code(info.clone())
777 .await
778 .map_err(|e|ApiError::bad_request(e.to_string()))
779 }
780 },
781 admin_api_endpoint! {
782 RESET_PEER_SETUP_CODES_ENDPOINT,
783 ApiVersion::new(0, 0),
784 async |config: &SetupApi, context, _v: ()| -> () {
785
786 config.reset_setup_codes().await;
787
788 Ok(())
789 }
790 },
791 admin_api_endpoint! {
792 GET_SETUP_CODE_ENDPOINT,
793 ApiVersion::new(0, 0),
794 async |config: &SetupApi, context, _request: ()| -> Option<String> {
795
796 Ok(config.setup_code().await)
797 }
798 },
799 admin_api_endpoint! {
800 START_DKG_ENDPOINT,
801 ApiVersion::new(0, 0),
802 async |config: &SetupApi, context, _v: ()| -> () {
803
804 config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
805 }
806 },
807 ]
808}
809
810#[cfg(test)]
811mod tests;