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")]
123#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault)]
124pub struct HoprSessionClientConfig {
125 pub forward_path: HopRouting,
127 pub return_path: HopRouting,
129 #[default(_code = "SessionCapability::Segmentation.into()")]
131 pub capabilities: SessionCapabilities,
132 #[default(None)]
134 pub pseudonym: Option<hopr_api::types::internal::protocol::HoprPseudonym>,
135 #[default(Some(SurbBalancerConfig::default()))]
137 pub surb_management: Option<SurbBalancerConfig>,
138 #[default(false)]
140 pub always_max_out_surbs: bool,
141}
142
143#[cfg(feature = "session-client")]
144impl From<HoprSessionClientConfig> for hopr_transport::SessionClientConfig {
145 fn from(value: HoprSessionClientConfig) -> Self {
146 Self {
147 forward_path_options: value.forward_path.into(),
148 return_path_options: value.return_path.into(),
149 capabilities: value.capabilities,
150 pseudonym: value.pseudonym,
151 surb_management: value.surb_management,
152 always_max_out_surbs: value.always_max_out_surbs,
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::Display, strum::EnumCount)]
159pub(crate) enum HoprLibProcess {
160 #[strum(to_string = "transport: {0}")]
161 Transport(HoprTransportProcess),
162 #[strum(to_string = "session server providing the exit node session stream functionality")]
163 #[allow(dead_code)] SessionServer,
165 #[strum(to_string = "subscription for on-chain channel updates")]
166 ChannelEvents,
167 #[strum(to_string = "on received ticket event (winning or rejected)")]
168 TicketEvents,
169 #[strum(to_string = "neglecting tickets on closed channels")]
170 ChannelClosureNeglect,
171}
172
173#[cfg(feature = "runtime-tokio")]
178pub fn prepare_tokio_runtime(
179 num_cpu_threads: Option<std::num::NonZeroUsize>,
180 num_io_threads: Option<std::num::NonZeroUsize>,
181) -> anyhow::Result<tokio::runtime::Runtime> {
182 use std::str::FromStr;
183 let avail_parallelism = std::thread::available_parallelism().ok().map(|v| v.get() / 2);
184
185 hopr_parallelize::cpu::init_thread_pool(
186 num_cpu_threads
187 .map(|v| v.get())
188 .or(avail_parallelism)
189 .ok_or(anyhow::anyhow!(
190 "Could not determine the number of CPU threads to use. Please set the HOPRD_NUM_CPU_THREADS \
191 environment variable."
192 ))?
193 .max(1),
194 )?;
195
196 Ok(tokio::runtime::Builder::new_multi_thread()
197 .enable_all()
198 .worker_threads(
199 num_io_threads
200 .map(|v| v.get())
201 .or(avail_parallelism)
202 .ok_or(anyhow::anyhow!(
203 "Could not determine the number of IO threads to use. Please set the HOPRD_NUM_IO_THREADS \
204 environment variable."
205 ))?
206 .max(1),
207 )
208 .thread_name("hoprd")
209 .thread_stack_size(
210 std::env::var("HOPRD_THREAD_STACK_SIZE")
211 .ok()
212 .and_then(|v| usize::from_str(&v).ok())
213 .unwrap_or(10 * 1024 * 1024)
214 .max(2 * 1024 * 1024),
215 )
216 .build()?)
217}
218
219pub type HoprTransportIO = hopr_transport::socket::HoprSocket<
221 futures::channel::mpsc::Receiver<ApplicationDataIn>,
222 futures::channel::mpsc::Sender<(DestinationRouting, ApplicationDataOut)>,
223>;
224
225type TicketEvents = (
226 async_broadcast::Sender<hopr_api::node::TicketEvent>,
227 async_broadcast::InactiveReceiver<hopr_api::node::TicketEvent>,
228);
229
230const NODE_READY_TIMEOUT: Duration = Duration::from_secs(120);
232
233pub struct Hopr<Chain, Graph, Net, TMgr> {
245 pub(crate) transport_id: OffchainKeypair,
246 pub(crate) chain_id: NodeOnchainIdentity,
247 pub(crate) cfg: config::HoprLibConfig,
248 pub(crate) state: Arc<AtomicHoprState>,
249 pub(crate) transport_api: HoprTransport<Chain, Graph, Net>,
250 pub(crate) chain_api: Chain,
251 pub(crate) ticket_event_subscribers: TicketEvents,
252 pub(crate) ticket_manager: TMgr,
253 #[allow(dead_code)] pub(crate) processes: AbortableList<HoprLibProcess>,
255}
256
257impl<Chain, Graph, Net, TMgr> Hopr<Chain, Graph, Net, TMgr>
258where
259 Chain: HoprChainApi + Clone + Send + Sync + 'static,
260 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey> + Clone + Send + Sync + 'static,
261 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
262 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
263 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
264 Net: NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
265{
266 pub fn config(&self) -> &config::HoprLibConfig {
267 &self.cfg
268 }
269
270 pub fn graph(&self) -> &Graph {
272 self.transport_api.graph()
273 }
274
275 #[cfg(feature = "session-client")]
276 fn error_if_not_in_state(&self, state: HoprState, error: String) -> errors::Result<()> {
277 if HoprNodeOperations::status(self) == state {
278 Ok(())
279 } else {
280 Err(HoprLibError::NotReady(state, error))
281 }
282 }
283}
284
285#[cfg(feature = "session-client")]
286#[async_trait::async_trait]
287impl<Chain, Graph, Net, TMgr> hopr_api::node::HoprSessionClientOperations for Hopr<Chain, Graph, Net, TMgr>
288where
289 Chain: HoprChainApi + Clone + Send + Sync + 'static,
290 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey> + Clone + Send + Sync + 'static,
291 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
292 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
293 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
294 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
295 TMgr: Send + Sync + 'static,
296{
297 type Config = HoprSessionClientConfig;
298 type Error = HoprLibError;
299 type Session = HoprSession;
300 type SessionConfigurator = HoprSessionConfigurator;
301 type Target = SessionTarget;
302
303 async fn connect_to(
304 &self,
305 destination: hopr_api::types::primitive::prelude::Address,
306 target: Self::Target,
307 cfg: Self::Config,
308 ) -> Result<(Self::Session, Self::SessionConfigurator), Self::Error> {
309 self.error_if_not_in_state(HoprState::Running, "Node is not ready for on-chain operations".into())?;
310
311 let backoff = backon::ConstantBuilder::default()
312 .with_max_times(self.cfg.protocol.session.establish_max_retries as usize)
313 .with_delay(self.cfg.protocol.session.establish_retry_timeout)
314 .with_jitter();
315
316 use backon::Retryable;
317
318 Ok((|| {
319 let cfg = hopr_transport::SessionClientConfig::from(cfg.clone());
320 let target = target.clone();
321 async { self.transport_api.new_session(destination, target, cfg).await }
322 })
323 .retry(backoff)
324 .sleep(backon::FuturesTimerSleeper)
325 .await?)
326 }
327}
328
329fn network_health_to_status(health: Health, component: &str) -> ComponentStatus {
335 match health {
336 Health::Green | Health::Yellow => ComponentStatus::Ready,
337 Health::Orange => ComponentStatus::Degraded(format!("{component}: low connectivity (1 peer)").into()),
338 Health::Red | Health::Unknown => {
340 ComponentStatus::Unavailable(format!("{component}: no connected peers").into())
341 }
342 }
343}
344
345impl<Chain, Graph, Net, TMgr> HasChainApi for Hopr<Chain, Graph, Net, TMgr>
346where
347 Chain: HoprChainApi + ComponentStatusReporter + Clone + Send + Sync + 'static,
348{
349 type ChainApi = Chain;
350 type ChainError = HoprLibError;
351
352 fn identity(&self) -> &NodeOnchainIdentity {
353 &self.chain_id
354 }
355
356 fn chain_api(&self) -> &Chain {
357 &self.chain_api
358 }
359
360 fn status(&self) -> ComponentStatus {
361 self.chain_api.component_status()
362 }
363
364 fn wait_for_on_chain_event<F>(
365 &self,
366 predicate: F,
367 context: String,
368 timeout: Duration,
369 ) -> EventWaitResult<<Self::ChainApi as HoprChainApi>::ChainError, Self::ChainError>
370 where
371 F: Fn(&ChainEvent) -> bool + Send + Sync + 'static,
372 {
373 debug!(%context, "registering wait for on-chain event");
374 let (event_stream, handle) = futures::stream::abortable(
375 self.chain_api
376 .subscribe()?
377 .skip_while(move |event| futures::future::ready(!predicate(event))),
378 );
379
380 let ctx = context.clone();
381
382 Ok((
383 spawn(async move {
384 pin_mut!(event_stream);
385 let res = event_stream
386 .next()
387 .timeout(futures_time::time::Duration::from(timeout))
388 .map_err(|_| {
389 HoprLibError::Timeout {
390 context: format!("{ctx} (after {timeout:?})"),
391 }
392 .into_right()
393 })
394 .await?
395 .ok_or(
396 HoprLibError::GeneralError(format!("on-chain event stream for {ctx} ended unexpectedly"))
397 .into_right(),
398 );
399 debug!(%ctx, ?res, "on-chain event waiting done");
400 res
401 })
402 .map_err(move |_| {
403 HoprLibError::GeneralError(format!("failed to spawn on-chain event wait for {context}")).into_right()
404 })
405 .and_then(futures::future::ready)
406 .boxed(),
407 handle,
408 ))
409 }
410}
411
412impl<Chain, Graph, Net, TMgr> HasNetworkView for Hopr<Chain, Graph, Net, TMgr>
413where
414 Chain: Send + Sync + 'static,
415 Graph: Send + Sync + 'static,
416 Net: hopr_api::network::NetworkView + Send + Sync + 'static,
417{
418 type NetworkView = HoprTransport<Chain, Graph, Net>;
419
420 fn network_view(&self) -> &Self::NetworkView {
421 &self.transport_api
422 }
423
424 fn status(&self) -> ComponentStatus {
425 network_health_to_status(self.transport_api.health(), "network")
426 }
427}
428
429impl<Chain, Graph, Net, TMgr> HasGraphView for Hopr<Chain, Graph, Net, TMgr>
430where
431 Chain: HoprChainApi + Clone + Send + Sync + 'static,
432 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey>
433 + hopr_api::graph::NetworkGraphConnectivity<NodeId = OffchainPublicKey>
434 + Clone
435 + Send
436 + Sync
437 + 'static,
438 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
439 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
440 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
441 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
442{
443 type Graph = Graph;
444
445 fn graph(&self) -> &Graph {
446 self.transport_api.graph()
447 }
448
449 fn status(&self) -> ComponentStatus {
450 ComponentStatus::Ready
451 }
452}
453
454impl<Chain, Graph, Net, TMgr> HasTransportApi for Hopr<Chain, Graph, Net, TMgr>
455where
456 Chain: HoprChainApi + Clone + Send + Sync + 'static,
457 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey> + Clone + Send + Sync + 'static,
458 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
459 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
460 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
461 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
462 TMgr: Send + Sync + 'static,
463{
464 type Transport = HoprTransport<Chain, Graph, Net>;
465
466 fn transport(&self) -> &Self::Transport {
467 &self.transport_api
468 }
469
470 fn status(&self) -> ComponentStatus {
471 network_health_to_status(self.transport_api.health(), "transport")
472 }
473}
474
475impl<Chain, Graph, Net, TMgr> HasTicketManagement for Hopr<Chain, Graph, Net, TMgr>
477where
478 Chain: HoprChainApi + Clone + Send + Sync + 'static,
479 TMgr: TicketManagement + Clone + Send + Sync + 'static,
480{
481 type TicketManager = TMgr;
482
483 fn ticket_management(&self) -> &TMgr {
484 &self.ticket_manager
485 }
486
487 fn subscribe_ticket_events(&self) -> impl Stream<Item = hopr_api::node::TicketEvent> + Send + 'static {
488 self.ticket_event_subscribers.1.activate_cloned()
489 }
490
491 fn status(&self) -> ComponentStatus {
492 ComponentStatus::Ready
493 }
494}
495
496impl<Chain, Graph, Net, TMgr> hopr_api::node::ActionableEventSource for Hopr<Chain, Graph, Net, TMgr>
497where
498 Chain: HoprChainApi + Send + Sync + 'static,
499 Graph: Send + Sync + 'static,
500 Net: hopr_api::network::NetworkView + Send + Sync + 'static,
501 TMgr: Send + Sync + 'static,
502{
503 fn subscribe_to_actionable_events(
504 &self,
505 filter: Option<&[ActionableEventDiscriminant]>,
506 ) -> Result<futures::stream::BoxStream<'static, ActionableEvent>, String> {
507 let wants = |d: ActionableEventDiscriminant| filter.is_none_or(|f| f.contains(&d));
508
509 let mut streams = Vec::<futures::stream::BoxStream<'static, ActionableEvent>>::new();
510
511 if wants(ActionableEventDiscriminant::Chain) {
512 streams.push(
513 self.chain_api
514 .subscribe()
515 .map_err(|e| e.to_string())?
516 .map(ActionableEvent::Chain)
517 .boxed(),
518 );
519 }
520
521 if wants(ActionableEventDiscriminant::Network) {
522 streams.push(
523 self.transport_api
524 .subscribe_network_events()
525 .map(ActionableEvent::Network)
526 .boxed(),
527 );
528 }
529
530 if wants(ActionableEventDiscriminant::Ticket) {
531 streams.push(
532 self.ticket_event_subscribers
533 .1
534 .activate_cloned()
535 .map(ActionableEvent::Ticket)
536 .boxed(),
537 );
538 }
539
540 if streams.is_empty() {
541 return Ok(futures::stream::empty().boxed());
542 }
543
544 Ok(streams.merge().boxed())
546 }
547}
548
549#[derive(Debug, Clone)]
551pub struct NodeComponentStatuses {
552 pub node_state: HoprState,
554 pub chain: ComponentStatus,
556 pub network: ComponentStatus,
558 pub transport: ComponentStatus,
560}
561
562impl NodeComponentStatuses {
563 pub fn aggregate(&self) -> ComponentStatus {
565 let statuses = [&self.chain, &self.network, &self.transport];
566 if statuses.iter().any(|s| s.is_unavailable()) {
567 ComponentStatus::Unavailable("one or more components unavailable".into())
568 } else if statuses.iter().any(|s| s.is_degraded()) {
569 ComponentStatus::Degraded("one or more components degraded".into())
570 } else if statuses.iter().any(|s| s.is_initializing()) {
571 ComponentStatus::Initializing("one or more components initializing".into())
572 } else {
573 ComponentStatus::Ready
574 }
575 }
576}
577
578impl<Chain, Graph, Net, TMgr> Hopr<Chain, Graph, Net, TMgr>
579where
580 Chain: HoprChainApi + ComponentStatusReporter + Clone + Send + Sync + 'static,
581 Net: hopr_api::network::NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
582 Graph: HoprGraphApi<HoprNodeId = OffchainPublicKey>
583 + hopr_api::graph::NetworkGraphConnectivity<NodeId = OffchainPublicKey>
584 + Clone
585 + Send
586 + Sync
587 + 'static,
588 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
589 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
590 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
591 TMgr: Send + Sync + 'static,
592{
593 pub fn component_statuses(&self) -> NodeComponentStatuses {
598 let base = self.state.load(Ordering::Relaxed);
599 let statuses = NodeComponentStatuses {
600 node_state: base,
601 chain: HasChainApi::status(self),
602 network: HasNetworkView::status(self),
603 transport: HasTransportApi::status(self),
604 };
605
606 if base == HoprState::Running {
608 NodeComponentStatuses {
609 node_state: match statuses.aggregate() {
610 ComponentStatus::Unavailable(_) => HoprState::Failed,
611 ComponentStatus::Degraded(_) | ComponentStatus::Initializing(_) => HoprState::Degraded,
612 ComponentStatus::Ready => HoprState::Running,
613 },
614 ..statuses
615 }
616 } else {
617 statuses
618 }
619 }
620}
621
622impl<Chain, Graph, Net, TMgr> Hopr<Chain, Graph, Net, TMgr> {
623 pub fn collect_hopr_metrics() -> errors::Result<String> {
625 cfg_if::cfg_if! {
626 if #[cfg(all(feature = "telemetry", not(test)))] {
627 hopr_metrics::gather_all_metrics().map_err(HoprLibError::other)
628 } else {
629 Err(HoprLibError::GeneralError("BUILT WITHOUT METRICS SUPPORT".into()))
630 }
631 }
632 }
633}
634
635impl<Chain, Graph, Net, TMgr> HoprNodeOperations for Hopr<Chain, Graph, Net, TMgr> {
636 fn status(&self) -> HoprState {
637 self.state.load(Ordering::Relaxed)
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
646 fn network_health_green_is_ready() {
647 assert_eq!(network_health_to_status(Health::Green, "test"), ComponentStatus::Ready);
648 }
649
650 #[test]
651 fn network_health_yellow_is_ready() {
652 assert_eq!(network_health_to_status(Health::Yellow, "test"), ComponentStatus::Ready);
653 }
654
655 #[test]
656 fn network_health_orange_is_degraded() {
657 assert!(network_health_to_status(Health::Orange, "network").is_degraded());
658 }
659
660 #[test]
661 fn network_health_red_is_unavailable() {
662 assert!(network_health_to_status(Health::Red, "network").is_unavailable());
663 }
664
665 #[test]
666 fn network_health_unknown_is_unavailable() {
667 assert!(network_health_to_status(Health::Unknown, "network").is_unavailable());
668 }
669
670 #[test]
671 fn aggregate_all_ready() {
672 let statuses = NodeComponentStatuses {
673 node_state: HoprState::Running,
674 chain: ComponentStatus::Ready,
675 network: ComponentStatus::Ready,
676 transport: ComponentStatus::Ready,
677 };
678 assert_eq!(statuses.aggregate(), ComponentStatus::Ready);
679 }
680
681 #[test]
682 fn aggregate_one_degraded() {
683 let statuses = NodeComponentStatuses {
684 node_state: HoprState::Running,
685 chain: ComponentStatus::Ready,
686 network: ComponentStatus::Degraded("low peers".into()),
687 transport: ComponentStatus::Ready,
688 };
689 assert!(statuses.aggregate().is_degraded());
690 }
691
692 #[test]
693 fn aggregate_one_unavailable() {
694 let statuses = NodeComponentStatuses {
695 node_state: HoprState::Running,
696 chain: ComponentStatus::Unavailable("blokli down".into()),
697 network: ComponentStatus::Ready,
698 transport: ComponentStatus::Ready,
699 };
700 assert!(statuses.aggregate().is_unavailable());
701 }
702
703 #[test]
704 fn aggregate_unavailable_wins_over_degraded() {
705 let statuses = NodeComponentStatuses {
706 node_state: HoprState::Running,
707 chain: ComponentStatus::Unavailable("blokli down".into()),
708 network: ComponentStatus::Degraded("low peers".into()),
709 transport: ComponentStatus::Ready,
710 };
711 assert!(statuses.aggregate().is_unavailable());
712 }
713
714 #[test]
715 fn aggregate_one_initializing() {
716 let statuses = NodeComponentStatuses {
717 node_state: HoprState::Running,
718 chain: ComponentStatus::Initializing("starting".into()),
719 network: ComponentStatus::Ready,
720 transport: ComponentStatus::Ready,
721 };
722 assert!(statuses.aggregate().is_initializing());
723 }
724
725 #[test]
726 fn aggregate_degraded_wins_over_initializing() {
727 let statuses = NodeComponentStatuses {
728 node_state: HoprState::Running,
729 chain: ComponentStatus::Initializing("starting".into()),
730 network: ComponentStatus::Degraded("low peers".into()),
731 transport: ComponentStatus::Ready,
732 };
733 assert!(statuses.aggregate().is_degraded());
734 }
735
736 #[test]
737 fn network_health_to_status_includes_component_name() {
738 match network_health_to_status(Health::Orange, "mycomp") {
739 ComponentStatus::Degraded(d) => assert!(d.contains("mycomp"), "detail should contain component name"),
740 other => panic!("expected Degraded, got {other:?}"),
741 }
742 }
743
744 #[test]
745 fn network_health_to_status_red_and_unknown_are_same_variant() {
746 let red = network_health_to_status(Health::Red, "x");
747 let unknown = network_health_to_status(Health::Unknown, "x");
748 assert!(red.is_unavailable());
749 assert!(unknown.is_unavailable());
750 }
751}
752
753pub fn peer_id_to_offchain_key(peer_id: &PeerId) -> errors::Result<OffchainPublicKey> {
757 Ok(hopr_transport::peer_id_to_public_key(peer_id)?)
758}