1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::net::SocketAddr;
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, bail, format_err};
7use bitcoin::hashes::sha256;
8use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
9pub use fedimint_core::config::{
10 ClientConfig, FederationId, GlobalClientConfig, JsonWithKind, ModuleInitRegistry, P2PMessage,
11 PeerUrl, ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
12};
13use fedimint_core::core::{ModuleInstanceId, ModuleKind};
14use fedimint_core::envs::is_running_in_test_env;
15use fedimint_core::invite_code::InviteCode;
16use fedimint_core::module::{
17 ApiAuth, ApiVersion, CORE_CONSENSUS_VERSION, CoreConsensusVersion, MultiApiVersion,
18 SupportedApiVersionsSummary, SupportedCoreApiVersions,
19};
20use fedimint_core::net::peers::{DynP2PConnections, Recipient};
21use fedimint_core::setup_code::{PeerEndpoints, PeerSetupCode};
22use fedimint_core::task::sleep;
23use fedimint_core::util::SafeUrl;
24use fedimint_core::{NumPeersExt, PeerId, secp256k1, timing};
25use fedimint_logging::LOG_NET_PEER_DKG;
26use fedimint_server_core::config::PeerHandleOpsExt as _;
27use fedimint_server_core::{DynServerModuleInit, ServerModuleInitRegistry};
28use futures::future::select_all;
29use hex::{FromHex, ToHex};
30use peer_handle::PeerHandle;
31use rand::rngs::OsRng;
32use secp256k1::{PublicKey, Secp256k1, SecretKey};
33use serde::{Deserialize, Serialize};
34use tokio::select;
35use tokio_rustls::rustls;
36use tracing::{error, info, warn};
37
38use crate::fedimint_core::encoding::Encodable;
39use crate::net::p2p::P2PStatusReceivers;
40use crate::net::p2p_connector::TlsConfig;
41
42pub mod dkg;
43pub mod io;
44pub mod peer_handle;
45pub mod setup;
46
47pub const DEFAULT_MAX_CLIENT_CONNECTIONS: u32 = 1000;
49
50const DEFAULT_BROADCAST_ROUND_DELAY_MS: u16 = 50;
52const DEFAULT_BROADCAST_ROUNDS_PER_SESSION: u16 = 3600;
53
54fn default_broadcast_rounds_per_session() -> u16 {
55 DEFAULT_BROADCAST_ROUNDS_PER_SESSION
56}
57
58const DEFAULT_TEST_BROADCAST_ROUND_DELAY_MS: u16 = 50;
60const DEFAULT_TEST_BROADCAST_ROUNDS_PER_SESSION: u16 = 200;
61
62#[allow(clippy::unsafe_derive_deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ServerConfig {
66 pub consensus: ServerConfigConsensus,
68 pub local: ServerConfigLocal,
70 pub private: ServerConfigPrivate,
73}
74
75impl ServerConfig {
76 pub fn iter_module_instances(
77 &self,
78 ) -> impl Iterator<Item = (ModuleInstanceId, &ModuleKind)> + '_ {
79 self.consensus.iter_module_instances()
80 }
81
82 pub(crate) fn supported_api_versions_summary(
83 modules: &BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
84 module_inits: &ServerModuleInitRegistry,
85 ) -> SupportedApiVersionsSummary {
86 SupportedApiVersionsSummary {
87 core: Self::supported_api_versions(),
88 modules: modules
89 .iter()
90 .map(|(&id, config)| {
91 (
92 id,
93 module_inits
94 .get(&config.kind)
95 .expect("missing module kind gen")
96 .supported_api_versions(),
97 )
98 })
99 .collect(),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ServerConfigPrivate {
106 pub api_auth: ApiAuth,
108 pub tls_key: Option<String>,
110 #[serde(default)]
112 pub iroh_api_sk: Option<iroh::SecretKey>,
113 #[serde(default)]
115 pub iroh_p2p_sk: Option<iroh::SecretKey>,
116 pub broadcast_secret_key: SecretKey,
118 pub modules: BTreeMap<ModuleInstanceId, JsonWithKind>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, Encodable)]
123pub struct ServerConfigConsensus {
124 pub code_version: String,
126 pub version: CoreConsensusVersion,
128 pub broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
130 #[serde(default = "default_broadcast_rounds_per_session")]
132 pub broadcast_rounds_per_session: u16,
133 pub api_endpoints: BTreeMap<PeerId, PeerUrl>,
135 #[serde(default)]
137 pub iroh_endpoints: BTreeMap<PeerId, PeerIrohEndpoints>,
138 pub tls_certs: BTreeMap<PeerId, String>,
140 pub modules: BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
142 pub meta: BTreeMap<String, String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, Encodable)]
147pub struct PeerIrohEndpoints {
148 pub name: String,
150 pub api_pk: iroh::PublicKey,
152 pub p2p_pk: iroh::PublicKey,
154}
155
156pub fn legacy_consensus_config_hash(cfg: &ServerConfigConsensus) -> sha256::Hash {
157 #[derive(Encodable)]
158 struct LegacyServerConfigConsensusHashMap {
159 code_version: String,
160 version: CoreConsensusVersion,
161 broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
162 broadcast_rounds_per_session: u16,
163 api_endpoints: BTreeMap<PeerId, PeerUrl>,
164 tls_certs: BTreeMap<PeerId, String>,
165 modules: BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
166 meta: BTreeMap<String, String>,
167 }
168
169 LegacyServerConfigConsensusHashMap {
170 code_version: cfg.code_version.clone(),
171 version: cfg.version,
172 broadcast_public_keys: cfg.broadcast_public_keys.clone(),
173 broadcast_rounds_per_session: cfg.broadcast_rounds_per_session,
174 api_endpoints: cfg.api_endpoints.clone(),
175 tls_certs: cfg.tls_certs.clone(),
176 modules: cfg.modules.clone(),
177 meta: cfg.meta.clone(),
178 }
179 .consensus_hash_sha256()
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ServerConfigLocal {
186 pub p2p_endpoints: BTreeMap<PeerId, PeerUrl>,
188 pub identity: PeerId,
190 pub max_connections: u32,
192 pub broadcast_round_delay_ms: u16,
197}
198
199#[derive(Debug, Clone)]
201pub struct ConfigGenSettings {
202 pub p2p_bind: SocketAddr,
204 pub api_bind: SocketAddr,
206 pub ui_bind: SocketAddr,
208 pub p2p_url: Option<SafeUrl>,
210 pub api_url: Option<SafeUrl>,
212 pub enable_iroh: bool,
214 pub iroh_dns: Option<SafeUrl>,
216 pub iroh_relays: Vec<SafeUrl>,
218 pub modules: ServerModuleConfigGenParamsRegistry,
220 pub registry: ServerModuleInitRegistry,
222}
223
224#[derive(Debug, Clone)]
225pub struct ConfigGenParams {
230 pub identity: PeerId,
232 pub tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
234 pub iroh_api_sk: Option<iroh::SecretKey>,
236 pub iroh_p2p_sk: Option<iroh::SecretKey>,
238 pub api_auth: ApiAuth,
240 pub peers: BTreeMap<PeerId, PeerSetupCode>,
242 pub meta: BTreeMap<String, String>,
244 pub disable_base_fees: bool,
246}
247
248impl ServerConfigConsensus {
249 pub fn api_endpoints(&self) -> BTreeMap<PeerId, PeerUrl> {
250 if self.iroh_endpoints.is_empty() {
251 self.api_endpoints.clone()
252 } else {
253 self.iroh_endpoints
254 .iter()
255 .map(|(peer, endpoints)| {
256 let url = PeerUrl {
257 name: endpoints.name.clone(),
258 url: SafeUrl::parse(&format!("iroh://{}", endpoints.api_pk))
259 .expect("Failed to parse iroh url"),
260 };
261
262 (*peer, url)
263 })
264 .collect()
265 }
266 }
267
268 pub fn iter_module_instances(
269 &self,
270 ) -> impl Iterator<Item = (ModuleInstanceId, &ModuleKind)> + '_ {
271 self.modules.iter().map(|(k, v)| (*k, &v.kind))
272 }
273
274 pub fn to_client_config(
275 &self,
276 module_config_gens: &ModuleInitRegistry<DynServerModuleInit>,
277 ) -> Result<ClientConfig, anyhow::Error> {
278 let client = ClientConfig {
279 global: GlobalClientConfig {
280 api_endpoints: self.api_endpoints(),
281 broadcast_public_keys: Some(self.broadcast_public_keys.clone()),
282 consensus_version: self.version,
283 meta: self.meta.clone(),
284 },
285 modules: self
286 .modules
287 .iter()
288 .map(|(k, v)| {
289 let r#gen = module_config_gens
290 .get(&v.kind)
291 .ok_or_else(|| format_err!("Module gen kind={} not found", v.kind))?;
292 Ok((*k, r#gen.get_client_config(*k, v)?))
293 })
294 .collect::<anyhow::Result<BTreeMap<_, _>>>()?,
295 };
296 Ok(client)
297 }
298}
299
300impl ServerConfig {
301 pub fn supported_api_versions() -> SupportedCoreApiVersions {
303 SupportedCoreApiVersions {
304 core_consensus: CORE_CONSENSUS_VERSION,
305 api: MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 8 }])
306 .expect("not version conflicts"),
307 }
308 }
309 pub fn from(
312 params: ConfigGenParams,
313 identity: PeerId,
314 broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
315 broadcast_secret_key: SecretKey,
316 modules: BTreeMap<ModuleInstanceId, ServerModuleConfig>,
317 code_version: String,
318 ) -> Self {
319 let consensus = ServerConfigConsensus {
320 code_version,
321 version: CORE_CONSENSUS_VERSION,
322 broadcast_public_keys,
323 broadcast_rounds_per_session: if is_running_in_test_env() {
324 DEFAULT_TEST_BROADCAST_ROUNDS_PER_SESSION
325 } else {
326 DEFAULT_BROADCAST_ROUNDS_PER_SESSION
327 },
328 api_endpoints: params.api_urls(),
329 iroh_endpoints: params.iroh_endpoints(),
330 tls_certs: params.tls_certs(),
331 modules: modules
332 .iter()
333 .map(|(peer, cfg)| (*peer, cfg.consensus.clone()))
334 .collect(),
335 meta: params.meta.clone(),
336 };
337
338 let local = ServerConfigLocal {
339 p2p_endpoints: params.p2p_urls(),
340 identity,
341 max_connections: DEFAULT_MAX_CLIENT_CONNECTIONS,
342 broadcast_round_delay_ms: if is_running_in_test_env() {
343 DEFAULT_TEST_BROADCAST_ROUND_DELAY_MS
344 } else {
345 DEFAULT_BROADCAST_ROUND_DELAY_MS
346 },
347 };
348
349 let private = ServerConfigPrivate {
350 api_auth: params.api_auth.clone(),
351 tls_key: params
352 .tls_key
353 .map(|key| key.secret_der().to_vec().encode_hex()),
354 iroh_api_sk: params.iroh_api_sk,
355 iroh_p2p_sk: params.iroh_p2p_sk,
356 broadcast_secret_key,
357 modules: modules
358 .iter()
359 .map(|(peer, cfg)| (*peer, cfg.private.clone()))
360 .collect(),
361 };
362
363 Self {
364 consensus,
365 local,
366 private,
367 }
368 }
369
370 pub fn get_invite_code(&self, api_secret: Option<String>) -> InviteCode {
371 InviteCode::new(
372 self.consensus.api_endpoints()[&self.local.identity]
373 .url
374 .clone(),
375 self.local.identity,
376 self.calculate_federation_id(),
377 api_secret,
378 )
379 }
380
381 pub fn calculate_federation_id(&self) -> FederationId {
382 FederationId(self.consensus.api_endpoints().consensus_hash())
383 }
384
385 pub fn get_module_config_typed<T: TypedServerModuleConfig>(
387 &self,
388 id: ModuleInstanceId,
389 ) -> anyhow::Result<T> {
390 let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
391 let consensus = self
392 .consensus
393 .modules
394 .get(&id)
395 .ok_or_else(|| format_err!("Typed module {id} not found"))?
396 .clone();
397 let module = ServerModuleConfig::from(private, consensus);
398
399 module.to_typed()
400 }
401 pub fn get_module_id_by_kind(
402 &self,
403 kind: impl Into<ModuleKind>,
404 ) -> anyhow::Result<ModuleInstanceId> {
405 let kind = kind.into();
406 Ok(*self
407 .consensus
408 .modules
409 .iter()
410 .find(|(_, v)| v.kind == kind)
411 .ok_or_else(|| format_err!("Module {kind} not found"))?
412 .0)
413 }
414
415 pub fn get_module_config(&self, id: ModuleInstanceId) -> anyhow::Result<ServerModuleConfig> {
417 let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
418 let consensus = self
419 .consensus
420 .modules
421 .get(&id)
422 .ok_or_else(|| format_err!("Module config {id} not found"))?
423 .clone();
424 Ok(ServerModuleConfig::from(private, consensus))
425 }
426
427 fn get_module_cfg_by_instance_id(
428 json: &BTreeMap<ModuleInstanceId, JsonWithKind>,
429 id: ModuleInstanceId,
430 ) -> anyhow::Result<JsonWithKind> {
431 Ok(json
432 .get(&id)
433 .ok_or_else(|| format_err!("Module cfg {id} not found"))
434 .cloned()?
435 .with_fixed_empty_value())
436 }
437
438 pub fn validate_config(
439 &self,
440 identity: &PeerId,
441 module_config_gens: &ServerModuleInitRegistry,
442 ) -> anyhow::Result<()> {
443 let endpoints = self.consensus.api_endpoints().clone();
444 let consensus = self.consensus.clone();
445 let private = self.private.clone();
446
447 let my_public_key = private.broadcast_secret_key.public_key(&Secp256k1::new());
448
449 if Some(&my_public_key) != consensus.broadcast_public_keys.get(identity) {
450 bail!("Broadcast secret key doesn't match corresponding public key");
451 }
452 if endpoints.keys().max().copied().map(PeerId::to_usize) != Some(endpoints.len() - 1) {
453 bail!("Peer ids are not indexed from 0");
454 }
455 if endpoints.keys().min().copied() != Some(PeerId::from(0)) {
456 bail!("Peer ids are not indexed from 0");
457 }
458
459 for (module_id, module_kind) in &self
460 .consensus
461 .modules
462 .iter()
463 .map(|(id, config)| Ok((*id, config.kind.clone())))
464 .collect::<anyhow::Result<BTreeSet<_>>>()?
465 {
466 module_config_gens
467 .get(module_kind)
468 .ok_or_else(|| format_err!("module config gen not found {module_kind}"))?
469 .validate_config(identity, self.get_module_config(*module_id)?)?;
470 }
471
472 Ok(())
473 }
474
475 pub fn trusted_dealer_gen(
476 modules: ServerModuleConfigGenParamsRegistry,
477 params: &HashMap<PeerId, ConfigGenParams>,
478 registry: &ServerModuleInitRegistry,
479 code_version_str: &str,
480 ) -> BTreeMap<PeerId, Self> {
481 let peer0 = ¶ms[&PeerId::from(0)];
482
483 let mut broadcast_pks = BTreeMap::new();
484 let mut broadcast_sks = BTreeMap::new();
485 for peer_id in peer0.peer_ids() {
486 let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
487 broadcast_pks.insert(peer_id, broadcast_pk);
488 broadcast_sks.insert(peer_id, broadcast_sk);
489 }
490
491 let module_configs: BTreeMap<_, _> = modules
492 .iter_modules()
493 .map(|(module_id, kind, module_params)| {
494 (
495 module_id,
496 registry
497 .get(kind)
498 .expect("Module not registered")
499 .trusted_dealer_gen(
500 &peer0.peer_ids(),
501 module_params,
502 params[&PeerId::from(0)].disable_base_fees,
503 ),
504 )
505 })
506 .collect();
507
508 let server_config: BTreeMap<_, _> = peer0
509 .peer_ids()
510 .iter()
511 .map(|&id| {
512 let config = ServerConfig::from(
513 params[&id].clone(),
514 id,
515 broadcast_pks.clone(),
516 *broadcast_sks.get(&id).expect("We created this entry"),
517 module_configs
518 .iter()
519 .map(|(module_id, cfgs)| (*module_id, cfgs[&id].clone()))
520 .collect(),
521 code_version_str.to_string(),
522 );
523 (id, config)
524 })
525 .collect();
526
527 server_config
528 }
529
530 pub async fn distributed_gen(
532 modules: ServerModuleConfigGenParamsRegistry,
533 params: &ConfigGenParams,
534 registry: ServerModuleInitRegistry,
535 code_version_str: String,
536 connections: DynP2PConnections<P2PMessage>,
537 mut p2p_status_receivers: P2PStatusReceivers,
538 ) -> anyhow::Result<Self> {
539 let _timing = timing::TimeReporter::new("distributed-gen").info();
540
541 if params.peer_ids().len() == 1 {
543 let server = Self::trusted_dealer_gen(
544 modules,
545 &HashMap::from([(params.identity, params.clone())]),
546 ®istry,
547 &code_version_str,
548 );
549
550 return Ok(server[¶ms.identity].clone());
551 }
552
553 info!(
554 target: LOG_NET_PEER_DKG,
555 "Waiting for all p2p connections to open..."
556 );
557
558 loop {
559 let mut pending_connection_receivers: Vec<_> = p2p_status_receivers
560 .iter_mut()
561 .filter_map(|(p, r)| {
562 r.mark_unchanged();
563 r.borrow().is_none().then_some((*p, r.clone()))
564 })
565 .collect();
566
567 if pending_connection_receivers.is_empty() {
568 break;
569 }
570
571 let disconnected_peers = pending_connection_receivers
572 .iter()
573 .map(|entry| entry.0)
574 .collect::<Vec<PeerId>>();
575
576 info!(
577 target: LOG_NET_PEER_DKG,
578 pending = ?disconnected_peers,
579 "Waiting for all p2p connections to open..."
580 );
581
582 select! {
583 _ = select_all(pending_connection_receivers.iter_mut().map(|r| Box::pin(r.1.changed()))) => {}
584 () = sleep(Duration::from_secs(10)) => {}
585 }
586 }
587
588 let checksum = params.peers.consensus_hash_sha256();
589
590 info!(
591 target: LOG_NET_PEER_DKG,
592 "Comparing connection codes checksum {checksum}..."
593 );
594
595 connections.send(Recipient::Everyone, P2PMessage::Checksum(checksum));
596
597 for peer in params
598 .peer_ids()
599 .into_iter()
600 .filter(|p| *p != params.identity)
601 {
602 let peer_message = connections
603 .receive_from_peer(peer)
604 .await
605 .context("Unexpected shutdown of p2p connections")?;
606
607 if peer_message != P2PMessage::Checksum(checksum) {
608 error!(
609 target: LOG_NET_PEER_DKG,
610 expected = ?P2PMessage::Checksum(checksum),
611 received = ?peer_message,
612 "Peer {peer} has sent invalid connection code checksum message"
613 );
614
615 bail!("Peer {peer} has sent invalid connection code checksum message");
616 }
617
618 info!(
619 target: LOG_NET_PEER_DKG,
620 "Peer {peer} has sent valid connection code checksum message"
621 );
622 }
623
624 info!(
625 target: LOG_NET_PEER_DKG,
626 "Running config generation..."
627 );
628
629 let handle = PeerHandle::new(
630 params.peer_ids().to_num_peers(),
631 params.identity,
632 &connections,
633 );
634
635 let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
636
637 let broadcast_public_keys = handle.exchange_encodable(broadcast_pk).await?;
638
639 let mut module_cfgs = BTreeMap::new();
640
641 for (module_id, kind, module_params) in modules.iter_modules() {
642 info!(
643 target: LOG_NET_PEER_DKG,
644 "Running config generation for module of kind {kind}..."
645 );
646
647 let cfg = registry
648 .get(kind)
649 .with_context(|| format!("Module of kind {kind} not found"))?
650 .distributed_gen(&handle, module_params, params.disable_base_fees)
651 .await?;
652
653 module_cfgs.insert(module_id, cfg);
654 }
655
656 let cfg = ServerConfig::from(
657 params.clone(),
658 params.identity,
659 broadcast_public_keys,
660 broadcast_sk,
661 module_cfgs,
662 code_version_str,
663 );
664
665 let checksum = cfg.consensus.consensus_hash_sha256();
666
667 info!(
668 target: LOG_NET_PEER_DKG,
669 "Comparing consensus config checksum {checksum}..."
670 );
671
672 connections.send(Recipient::Everyone, P2PMessage::Checksum(checksum));
673
674 for peer in params
675 .peer_ids()
676 .into_iter()
677 .filter(|p| *p != params.identity)
678 {
679 let peer_message = connections
680 .receive_from_peer(peer)
681 .await
682 .context("Unexpected shutdown of p2p connections")?;
683
684 if peer_message != P2PMessage::Checksum(checksum) {
685 warn!(
686 target: LOG_NET_PEER_DKG,
687 expected = ?P2PMessage::Checksum(checksum),
688 received = ?peer_message,
689 config = ?cfg.consensus,
690 "Peer {peer} has sent invalid consensus config checksum message"
691 );
692
693 bail!("Peer {peer} has sent invalid consensus config checksum message");
694 }
695
696 info!(
697 target: LOG_NET_PEER_DKG,
698 "Peer {peer} has sent valid consensus config checksum message"
699 );
700 }
701
702 info!(
703 target: LOG_NET_PEER_DKG,
704 "Config generation has completed successfully!"
705 );
706
707 Ok(cfg)
708 }
709}
710
711impl ServerConfig {
712 pub fn tls_config(&self) -> TlsConfig {
713 TlsConfig {
714 private_key: Arc::new(
715 rustls::pki_types::PrivateKeyDer::try_from(
716 Vec::from_hex(self.private.tls_key.clone().unwrap()).unwrap(),
717 )
718 .expect("Failed to parse private key"),
719 ),
720 certificates: self
721 .consensus
722 .tls_certs
723 .iter()
724 .map(|(peer, cert)| {
725 (
726 *peer,
727 rustls::pki_types::CertificateDer::from(Vec::from_hex(cert).unwrap()),
728 )
729 })
730 .collect(),
731 peer_names: self
732 .local
733 .p2p_endpoints
734 .iter()
735 .map(|(id, endpoint)| (*id, endpoint.name.to_string()))
736 .collect(),
737 }
738 }
739}
740
741impl ConfigGenParams {
742 pub fn peer_ids(&self) -> Vec<PeerId> {
743 self.peers.keys().copied().collect()
744 }
745
746 pub fn tls_config(&self) -> TlsConfig {
747 TlsConfig {
748 private_key: self.tls_key.clone().unwrap(),
749 certificates: self
750 .tls_certs()
751 .iter()
752 .map(|(peer, cert)| {
753 (
754 *peer,
755 rustls::pki_types::CertificateDer::from(Vec::from_hex(cert).unwrap()),
756 )
757 })
758 .collect(),
759 peer_names: self
760 .p2p_urls()
761 .into_iter()
762 .map(|(id, peer)| (id, peer.name))
763 .collect(),
764 }
765 }
766
767 pub fn tls_certs(&self) -> BTreeMap<PeerId, String> {
768 self.peers
769 .iter()
770 .filter_map(|(id, peer)| {
771 match peer.endpoints.clone() {
772 PeerEndpoints::Tcp { cert, .. } => Some(cert.encode_hex()),
773 PeerEndpoints::Iroh { .. } => None,
774 }
775 .map(|peer| (*id, peer))
776 })
777 .collect()
778 }
779
780 pub fn p2p_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
781 self.peers
782 .iter()
783 .filter_map(|(id, peer)| {
784 match peer.endpoints.clone() {
785 PeerEndpoints::Tcp { p2p_url, .. } => Some(PeerUrl {
786 name: peer.name.clone(),
787 url: p2p_url.clone(),
788 }),
789 PeerEndpoints::Iroh { .. } => None,
790 }
791 .map(|peer| (*id, peer))
792 })
793 .collect()
794 }
795
796 pub fn api_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
797 self.peers
798 .iter()
799 .filter_map(|(id, peer)| {
800 match peer.endpoints.clone() {
801 PeerEndpoints::Tcp { api_url, .. } => Some(PeerUrl {
802 name: peer.name.clone(),
803 url: api_url.clone(),
804 }),
805 PeerEndpoints::Iroh { .. } => None,
806 }
807 .map(|peer| (*id, peer))
808 })
809 .collect()
810 }
811
812 pub fn iroh_endpoints(&self) -> BTreeMap<PeerId, PeerIrohEndpoints> {
813 self.peers
814 .iter()
815 .filter_map(|(id, peer)| {
816 match peer.endpoints.clone() {
817 PeerEndpoints::Tcp { .. } => None,
818 PeerEndpoints::Iroh { api_pk, p2p_pk } => Some(PeerIrohEndpoints {
819 name: peer.name.clone(),
820 api_pk,
821 p2p_pk,
822 }),
823 }
824 .map(|peer| (*id, peer))
825 })
826 .collect()
827 }
828}