1mod helpers;
17
18pub mod builder;
20pub mod config;
22pub mod constants;
24pub mod errors;
26pub use hopr_api::node::{AnnouncedPeer, AnnouncementOrigin};
28pub mod utils;
30
31pub use hopr_api as api;
32
33#[doc(hidden)]
35pub mod exports {
36 pub mod types {
37 pub use hopr_api::types::{chain, internal, primitive};
38 }
39
40 pub mod crypto {
41 pub use hopr_api::types::crypto as types;
42 pub use hopr_crypto_keypair as keypair;
43 }
44
45 pub mod network {
46 pub use hopr_network_types as types;
47 }
48
49 pub use hopr_transport as transport;
50}
51
52#[doc(hidden)]
54pub mod prelude {
55 #[cfg(feature = "runtime-tokio")]
56 pub use super::exports::network::types::{
57 prelude::ForeignDataMode,
58 udp::{ConnectedUdpStream, UdpStreamParallelism},
59 };
60 pub use super::exports::{
61 crypto::{
62 keypair::key_pair::HoprKeys,
63 types::prelude::{ChainKeypair, Hash, OffchainKeypair},
64 },
65 transport::{OffchainPublicKey, socket::HoprSocket},
66 types::primitive::prelude::Address,
67 };
68}
69
70use std::{
71 sync::{Arc, atomic::Ordering},
72 time::Duration,
73};
74
75use futures::{FutureExt, Stream, StreamExt, TryFutureExt, pin_mut};
76use futures_time::future::FutureExt as FuturesTimeFutureExt;
77#[cfg(feature = "session-client")]
78pub use hopr_api::node::HoprSessionClientOperations;
79use hopr_api::{
80 chain::*,
81 graph::HoprGraphApi,
82 network::NetworkView as _,
83 node::{
84 AtomicHoprState, ComponentStatus, ComponentStatusReporter, EitherErrExt, EventWaitResult, HasChainApi,
85 HasGraphView, HasNetworkView, HasTicketManagement, HasTransportApi, NodeOnchainIdentity,
86 },
87};
88pub use hopr_api::{
89 graph::EdgeLinkObservable,
90 network::NetworkStreamControl,
91 node::{
92 EitherErr, HoprNodeOperations, HoprState, IncentiveChannelOperations, IncentiveRedeemOperations,
93 TransportOperations,
94 },
95 tickets::{ChannelStats, RedemptionResult, TicketManagement, TicketManagementExt},
96 types::{crypto::prelude::*, internal::prelude::*, primitive::prelude::*},
97};
98use hopr_async_runtime::prelude::spawn;
99pub use hopr_async_runtime::{Abortable, AbortableList};
100pub use hopr_crypto_keypair::key_pair::{HoprKeys, IdentityRetrievalModes};
101pub use hopr_network_types::prelude::*;
102#[cfg(feature = "runtime-tokio")]
103pub use hopr_transport::transfer_session;
104pub use hopr_transport::*;
105use tracing::debug;
106
107pub use crate::{
108 config::SafeModule,
109 constants::{MIN_NATIVE_BALANCE, SUGGESTED_NATIVE_BALANCE},
110 errors::{HoprLibError, HoprStatusError},
111};
112
113#[cfg(feature = "session-client")]
117#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, smart_default::SmartDefault)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119pub struct HopRouting(
120 #[default(hopr_api::types::primitive::bounded::BoundedSize::MIN)]
121 hopr_api::types::primitive::bounded::BoundedSize<
122 { hopr_api::types::internal::routing::RoutingOptions::MAX_INTERMEDIATE_HOPS },
123 >,
124);
125
126#[cfg(feature = "session-client")]
127impl HopRouting {
128 pub const MAX_HOPS: usize = hopr_api::types::internal::routing::RoutingOptions::MAX_INTERMEDIATE_HOPS;
130
131 pub fn hop_count(self) -> usize {
133 self.0.into()
134 }
135}
136
137#[cfg(feature = "session-client")]
138impl TryFrom<usize> for HopRouting {
139 type Error = hopr_api::types::primitive::errors::GeneralError;
140
141 fn try_from(value: usize) -> Result<Self, Self::Error> {
142 Ok(Self(value.try_into()?))
143 }
144}
145
146#[cfg(feature = "session-client")]
147impl From<HopRouting> for hopr_api::types::internal::routing::RoutingOptions {
148 fn from(value: HopRouting) -> Self {
149 Self::Hops(value.0)
150 }
151}
152
153#[cfg(feature = "session-client")]
158#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault)]
159pub struct HoprSessionClientConfig {
160 pub forward_path: HopRouting,
162 pub return_path: HopRouting,
164 #[default(_code = "SessionCapability::Segmentation.into()")]
166 pub capabilities: SessionCapabilities,
167 #[default(None)]
169 pub pseudonym: Option<hopr_api::types::internal::protocol::HoprPseudonym>,
170 #[default(Some(SurbBalancerConfig::default()))]
172 pub surb_management: Option<SurbBalancerConfig>,
173 #[default(false)]
175 pub always_max_out_surbs: bool,
176}
177
178#[cfg(feature = "session-client")]
179impl From<HoprSessionClientConfig> for hopr_transport::SessionClientConfig {
180 fn from(value: HoprSessionClientConfig) -> Self {
181 Self {
182 forward_path_options: value.forward_path.into(),
183 return_path_options: value.return_path.into(),
184 capabilities: value.capabilities,
185 pseudonym: value.pseudonym,
186 surb_management: value.surb_management,
187 always_max_out_surbs: value.always_max_out_surbs,
188 }
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::Display, strum::EnumCount)]
194pub(crate) enum HoprLibProcess {
195 #[strum(to_string = "transport: {0}")]
196 Transport(HoprTransportProcess),
197 #[strum(to_string = "session server providing the exit node session stream functionality")]
198 #[allow(dead_code)] SessionServer,
200 #[strum(to_string = "subscription for on-chain channel updates")]
201 ChannelEvents,
202 #[strum(to_string = "on received ticket event (winning or rejected)")]
203 TicketEvents,
204 #[strum(to_string = "neglecting tickets on closed channels")]
205 ChannelClosureNeglect,
206}
207
208#[cfg(feature = "runtime-tokio")]
213pub fn prepare_tokio_runtime(
214 num_cpu_threads: Option<std::num::NonZeroUsize>,
215 num_io_threads: Option<std::num::NonZeroUsize>,
216) -> anyhow::Result<tokio::runtime::Runtime> {
217 use std::str::FromStr;
218 let avail_parallelism = std::thread::available_parallelism().ok().map(|v| v.get() / 2);
219
220 hopr_parallelize::cpu::init_thread_pool(
221 num_cpu_threads
222 .map(|v| v.get())
223 .or(avail_parallelism)
224 .ok_or(anyhow::anyhow!(
225 "Could not determine the number of CPU threads to use. Please set the HOPRD_NUM_CPU_THREADS \
226 environment variable."
227 ))?
228 .max(1),
229 )?;
230
231 Ok(tokio::runtime::Builder::new_multi_thread()
232 .enable_all()
233 .worker_threads(
234 num_io_threads
235 .map(|v| v.get())
236 .or(avail_parallelism)
237 .ok_or(anyhow::anyhow!(
238 "Could not determine the number of IO threads to use. Please set the HOPRD_NUM_IO_THREADS \
239 environment variable."
240 ))?
241 .max(1),
242 )
243 .thread_name("hoprd")
244 .thread_stack_size(
245 std::env::var("HOPRD_THREAD_STACK_SIZE")
246 .ok()
247 .and_then(|v| usize::from_str(&v).ok())
248 .unwrap_or(10 * 1024 * 1024)
249 .max(2 * 1024 * 1024),
250 )
251 .build()?)
252}
253
254pub type HoprTransportIO = socket::HoprSocket<
256 futures::channel::mpsc::Receiver<ApplicationDataIn>,
257 futures::channel::mpsc::Sender<(DestinationRouting, ApplicationDataOut)>,
258>;
259
260type TicketEvents = (
261 async_broadcast::Sender<hopr_api::node::TicketEvent>,
262 async_broadcast::InactiveReceiver<hopr_api::node::TicketEvent>,
263);
264
265const NODE_READY_TIMEOUT: Duration = Duration::from_secs(120);
267
268pub struct Hopr<Chain, Graph, Net, TMgr> {
280 pub(crate) transport_id: OffchainKeypair,
281 pub(crate) chain_id: NodeOnchainIdentity,
282 pub(crate) cfg: config::HoprLibConfig,
283 pub(crate) state: Arc<AtomicHoprState>,
284 pub(crate) transport_api: HoprTransport<Chain, Graph, Net>,
285 pub(crate) chain_api: Chain,
286 pub(crate) ticket_event_subscribers: TicketEvents,
287 pub(crate) ticket_manager: TMgr,
288 #[allow(dead_code)] pub(crate) processes: AbortableList<HoprLibProcess>,
290}
291
292impl<Chain, Graph, Net, TMgr> Hopr<Chain, Graph, Net, TMgr>
293where
294 Chain: HoprChainApi + Clone + Send + Sync + 'static,
295 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey> + Clone + Send + Sync + 'static,
296 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
297 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
298 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
299 Net: NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
300{
301 pub fn config(&self) -> &config::HoprLibConfig {
302 &self.cfg
303 }
304
305 pub fn graph(&self) -> &Graph {
307 self.transport_api.graph()
308 }
309
310 #[cfg(feature = "session-client")]
311 fn error_if_not_in_state(&self, state: HoprState, error: String) -> errors::Result<()> {
312 if HoprNodeOperations::status(self) == state {
313 Ok(())
314 } else {
315 Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(state, error)))
316 }
317 }
318}
319
320#[cfg(feature = "session-client")]
321#[async_trait::async_trait]
322impl<Chain, Graph, Net, TMgr> hopr_api::node::HoprSessionClientOperations for Hopr<Chain, Graph, Net, TMgr>
323where
324 Chain: HoprChainApi + Clone + Send + Sync + 'static,
325 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey> + Clone + Send + Sync + 'static,
326 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
327 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
328 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
329 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
330 TMgr: Send + Sync + 'static,
331{
332 type Config = HoprSessionClientConfig;
333 type Error = HoprLibError;
334 type Session = HoprSession;
335 type SessionConfigurator = HoprSessionConfigurator;
336 type Target = SessionTarget;
337
338 async fn connect_to(
339 &self,
340 destination: Address,
341 target: Self::Target,
342 cfg: Self::Config,
343 ) -> Result<(Self::Session, Self::SessionConfigurator), Self::Error> {
344 self.error_if_not_in_state(HoprState::Running, "Node is not ready for on-chain operations".into())?;
345
346 let backoff = backon::ConstantBuilder::default()
347 .with_max_times(self.cfg.protocol.session.establish_max_retries as usize)
348 .with_delay(self.cfg.protocol.session.establish_retry_timeout)
349 .with_jitter();
350
351 use backon::Retryable;
352
353 Ok((|| {
354 let cfg = hopr_transport::SessionClientConfig::from(cfg.clone());
355 let target = target.clone();
356 async { self.transport_api.new_session(destination, target, cfg).await }
357 })
358 .retry(backoff)
359 .sleep(backon::FuturesTimerSleeper)
360 .await?)
361 }
362}
363
364fn network_health_to_status(health: Health, component: &str) -> ComponentStatus {
370 match health {
371 Health::Green | Health::Yellow => ComponentStatus::Ready,
372 Health::Orange => ComponentStatus::Degraded(format!("{component}: low connectivity (1 peer)").into()),
373 Health::Red | Health::Unknown => {
375 ComponentStatus::Unavailable(format!("{component}: no connected peers").into())
376 }
377 }
378}
379
380impl<Chain, Graph, Net, TMgr> HasChainApi for Hopr<Chain, Graph, Net, TMgr>
381where
382 Chain: HoprChainApi + ComponentStatusReporter + Clone + Send + Sync + 'static,
383{
384 type ChainApi = Chain;
385 type ChainError = HoprLibError;
386
387 fn identity(&self) -> &NodeOnchainIdentity {
388 &self.chain_id
389 }
390
391 fn chain_api(&self) -> &Chain {
392 &self.chain_api
393 }
394
395 fn status(&self) -> ComponentStatus {
396 self.chain_api.component_status()
397 }
398
399 fn wait_for_on_chain_event<F>(
400 &self,
401 predicate: F,
402 context: String,
403 timeout: Duration,
404 ) -> EventWaitResult<<Self::ChainApi as HoprChainApi>::ChainError, Self::ChainError>
405 where
406 F: Fn(&ChainEvent) -> bool + Send + Sync + 'static,
407 {
408 debug!(%context, "registering wait for on-chain event");
409 let (event_stream, handle) = futures::stream::abortable(
410 self.chain_api
411 .subscribe()?
412 .skip_while(move |event| futures::future::ready(!predicate(event))),
413 );
414
415 let ctx = context.clone();
416
417 Ok((
418 spawn(async move {
419 pin_mut!(event_stream);
420 let res = event_stream
421 .next()
422 .timeout(futures_time::time::Duration::from(timeout))
423 .map_err(|_| HoprLibError::GeneralError(format!("{ctx} timed out after {timeout:?}")).into_right())
424 .await?
425 .ok_or(
426 HoprLibError::GeneralError(format!("failed to yield an on-chain event for {ctx}")).into_right(),
427 );
428 debug!(%ctx, ?res, "on-chain event waiting done");
429 res
430 })
431 .map_err(move |_| HoprLibError::GeneralError(format!("failed to spawn future for {context}")).into_right())
432 .and_then(futures::future::ready)
433 .boxed(),
434 handle,
435 ))
436 }
437}
438
439impl<Chain, Graph, Net, TMgr> HasNetworkView for Hopr<Chain, Graph, Net, TMgr>
440where
441 Chain: Send + Sync + 'static,
442 Graph: Send + Sync + 'static,
443 Net: hopr_api::network::NetworkView + Send + Sync + 'static,
444{
445 type NetworkView = HoprTransport<Chain, Graph, Net>;
446
447 fn network_view(&self) -> &Self::NetworkView {
448 &self.transport_api
449 }
450
451 fn status(&self) -> ComponentStatus {
452 network_health_to_status(self.transport_api.health(), "network")
453 }
454}
455
456impl<Chain, Graph, Net, TMgr> HasGraphView for Hopr<Chain, Graph, Net, TMgr>
457where
458 Chain: HoprChainApi + Clone + Send + Sync + 'static,
459 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey>
460 + hopr_api::graph::NetworkGraphConnectivity<NodeId = OffchainPublicKey>
461 + Clone
462 + Send
463 + Sync
464 + 'static,
465 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
466 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
467 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
468 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
469{
470 type Graph = Graph;
471
472 fn graph(&self) -> &Graph {
473 self.transport_api.graph()
474 }
475
476 fn status(&self) -> ComponentStatus {
477 ComponentStatus::Ready
478 }
479}
480
481impl<Chain, Graph, Net, TMgr> HasTransportApi for Hopr<Chain, Graph, Net, TMgr>
482where
483 Chain: HoprChainApi + Clone + Send + Sync + 'static,
484 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey> + Clone + Send + Sync + 'static,
485 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
486 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
487 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
488 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
489 TMgr: Send + Sync + 'static,
490{
491 type Transport = HoprTransport<Chain, Graph, Net>;
492
493 fn transport(&self) -> &Self::Transport {
494 &self.transport_api
495 }
496
497 fn status(&self) -> ComponentStatus {
498 network_health_to_status(self.transport_api.health(), "transport")
499 }
500}
501
502impl<Chain, Graph, Net, TMgr> HasTicketManagement for Hopr<Chain, Graph, Net, TMgr>
504where
505 Chain: HoprChainApi + Clone + Send + Sync + 'static,
506 TMgr: TicketManagement + Clone + Send + Sync + 'static,
507{
508 type TicketManager = TMgr;
509
510 fn ticket_management(&self) -> &TMgr {
511 &self.ticket_manager
512 }
513
514 fn subscribe_ticket_events(&self) -> impl Stream<Item = hopr_api::node::TicketEvent> + Send + 'static {
515 self.ticket_event_subscribers.1.activate_cloned()
516 }
517
518 fn status(&self) -> ComponentStatus {
519 ComponentStatus::Ready
520 }
521}
522
523#[derive(Debug, Clone)]
525pub struct NodeComponentStatuses {
526 pub node_state: HoprState,
528 pub chain: ComponentStatus,
530 pub network: ComponentStatus,
532 pub transport: ComponentStatus,
534}
535
536impl NodeComponentStatuses {
537 pub fn aggregate(&self) -> ComponentStatus {
539 let statuses = [&self.chain, &self.network, &self.transport];
540 if statuses.iter().any(|s| s.is_unavailable()) {
541 ComponentStatus::Unavailable("one or more components unavailable".into())
542 } else if statuses.iter().any(|s| s.is_degraded()) {
543 ComponentStatus::Degraded("one or more components degraded".into())
544 } else if statuses.iter().any(|s| s.is_initializing()) {
545 ComponentStatus::Initializing("one or more components initializing".into())
546 } else {
547 ComponentStatus::Ready
548 }
549 }
550}
551
552impl<Chain, Graph, Net, TMgr> Hopr<Chain, Graph, Net, TMgr>
553where
554 Chain: HoprChainApi + ComponentStatusReporter + Clone + Send + Sync + 'static,
555 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
556 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey>
557 + hopr_api::graph::NetworkGraphConnectivity<NodeId = OffchainPublicKey>
558 + Clone
559 + Send
560 + Sync
561 + 'static,
562 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
563 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
564 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
565 TMgr: Send + Sync + 'static,
566{
567 pub fn component_statuses(&self) -> NodeComponentStatuses {
572 let base = self.state.load(Ordering::Relaxed);
573 let statuses = NodeComponentStatuses {
574 node_state: base,
575 chain: HasChainApi::status(self),
576 network: HasNetworkView::status(self),
577 transport: HasTransportApi::status(self),
578 };
579
580 if base == HoprState::Running {
582 NodeComponentStatuses {
583 node_state: match statuses.aggregate() {
584 ComponentStatus::Unavailable(_) => HoprState::Failed,
585 ComponentStatus::Degraded(_) | ComponentStatus::Initializing(_) => HoprState::Degraded,
586 ComponentStatus::Ready => HoprState::Running,
587 },
588 ..statuses
589 }
590 } else {
591 statuses
592 }
593 }
594}
595
596impl<Chain, Graph, Net, TMgr> Hopr<Chain, Graph, Net, TMgr> {
597 pub fn collect_hopr_metrics() -> errors::Result<String> {
599 cfg_if::cfg_if! {
600 if #[cfg(all(feature = "telemetry", not(test)))] {
601 hopr_metrics::gather_all_metrics().map_err(HoprLibError::other)
602 } else {
603 Err(HoprLibError::GeneralError("BUILT WITHOUT METRICS SUPPORT".into()))
604 }
605 }
606 }
607}
608
609impl<Chain, Graph, Net, TMgr> HoprNodeOperations for Hopr<Chain, Graph, Net, TMgr> {
610 fn status(&self) -> HoprState {
611 self.state.load(Ordering::Relaxed)
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn network_health_green_is_ready() {
621 assert_eq!(network_health_to_status(Health::Green, "test"), ComponentStatus::Ready);
622 }
623
624 #[test]
625 fn network_health_yellow_is_ready() {
626 assert_eq!(network_health_to_status(Health::Yellow, "test"), ComponentStatus::Ready);
627 }
628
629 #[test]
630 fn network_health_orange_is_degraded() {
631 assert!(network_health_to_status(Health::Orange, "network").is_degraded());
632 }
633
634 #[test]
635 fn network_health_red_is_unavailable() {
636 assert!(network_health_to_status(Health::Red, "network").is_unavailable());
637 }
638
639 #[test]
640 fn network_health_unknown_is_unavailable() {
641 assert!(network_health_to_status(Health::Unknown, "network").is_unavailable());
642 }
643
644 #[test]
645 fn aggregate_all_ready() {
646 let statuses = NodeComponentStatuses {
647 node_state: HoprState::Running,
648 chain: ComponentStatus::Ready,
649 network: ComponentStatus::Ready,
650 transport: ComponentStatus::Ready,
651 };
652 assert_eq!(statuses.aggregate(), ComponentStatus::Ready);
653 }
654
655 #[test]
656 fn aggregate_one_degraded() {
657 let statuses = NodeComponentStatuses {
658 node_state: HoprState::Running,
659 chain: ComponentStatus::Ready,
660 network: ComponentStatus::Degraded("low peers".into()),
661 transport: ComponentStatus::Ready,
662 };
663 assert!(statuses.aggregate().is_degraded());
664 }
665
666 #[test]
667 fn aggregate_one_unavailable() {
668 let statuses = NodeComponentStatuses {
669 node_state: HoprState::Running,
670 chain: ComponentStatus::Unavailable("blokli down".into()),
671 network: ComponentStatus::Ready,
672 transport: ComponentStatus::Ready,
673 };
674 assert!(statuses.aggregate().is_unavailable());
675 }
676
677 #[test]
678 fn aggregate_unavailable_wins_over_degraded() {
679 let statuses = NodeComponentStatuses {
680 node_state: HoprState::Running,
681 chain: ComponentStatus::Unavailable("blokli down".into()),
682 network: ComponentStatus::Degraded("low peers".into()),
683 transport: ComponentStatus::Ready,
684 };
685 assert!(statuses.aggregate().is_unavailable());
686 }
687
688 #[test]
689 fn aggregate_one_initializing() {
690 let statuses = NodeComponentStatuses {
691 node_state: HoprState::Running,
692 chain: ComponentStatus::Initializing("starting".into()),
693 network: ComponentStatus::Ready,
694 transport: ComponentStatus::Ready,
695 };
696 assert!(statuses.aggregate().is_initializing());
697 }
698
699 #[test]
700 fn aggregate_degraded_wins_over_initializing() {
701 let statuses = NodeComponentStatuses {
702 node_state: HoprState::Running,
703 chain: ComponentStatus::Initializing("starting".into()),
704 network: ComponentStatus::Degraded("low peers".into()),
705 transport: ComponentStatus::Ready,
706 };
707 assert!(statuses.aggregate().is_degraded());
708 }
709
710 #[test]
711 fn network_health_to_status_includes_component_name() {
712 match network_health_to_status(Health::Orange, "mycomp") {
713 ComponentStatus::Degraded(d) => assert!(d.contains("mycomp"), "detail should contain component name"),
714 other => panic!("expected Degraded, got {other:?}"),
715 }
716 }
717
718 #[test]
719 fn network_health_to_status_red_and_unknown_are_same_variant() {
720 let red = network_health_to_status(Health::Red, "x");
721 let unknown = network_health_to_status(Health::Unknown, "x");
722 assert!(red.is_unavailable());
723 assert!(unknown.is_unavailable());
724 }
725}
726
727pub fn peer_id_to_offchain_key(peer_id: &PeerId) -> errors::Result<OffchainPublicKey> {
731 Ok(hopr_transport::peer_id_to_public_key(peer_id)?)
732}