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