Skip to main content

hopr_lib/
lib.rs

1//! HOPR library creating a unified [`Hopr`] object that can be used on its own,
2//! as well as integrated into other systems and libraries.
3//!
4//! The [`Hopr`] object is standalone, meaning that once it is constructed and run,
5//! it will perform its functionality autonomously. The API it offers serves as a
6//! high-level integration point for other applications and utilities, but offers
7//! a complete and fully featured HOPR node stripped from top level functionality
8//! such as the REST API, key management...
9//!
10//! The intended way to use hopr_lib is for a specific tool to be built on top of it;
11//! should the default `hoprd` implementation not be acceptable.
12//!
13//! For most of the practical use cases, the `hoprd` application should be a preferable
14//! choice.
15/// Helper functions.
16mod helpers;
17
18/// Builder module for the [`Hopr`] object.
19pub mod builder;
20/// Configuration-related public types
21pub mod config;
22/// Various public constants.
23pub mod constants;
24/// Lists all errors thrown from this library.
25pub mod errors;
26// Re-export peer discovery types from hopr-api.
27pub use hopr_api::node::{AnnouncedPeer, AnnouncementOrigin};
28/// Utility module with helper types and functionality over hopr-lib behavior.
29pub mod utils;
30
31pub use hopr_api as api;
32
33/// Exports of libraries necessary for API and interface operations.
34#[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/// Export of relevant types for easier integration.
53#[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/// Public routing configuration for session opening in `hopr-lib`.
114///
115/// This intentionally exposes only hop-count based routing.
116#[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    /// Maximum number of hops that can be configured.
129    pub const MAX_HOPS: usize = hopr_api::types::internal::routing::RoutingOptions::MAX_INTERMEDIATE_HOPS;
130
131    /// Returns the configured number of hops.
132    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/// Session client configuration for `hopr-lib`.
154///
155/// Unlike transport-level configuration, this API intentionally does not expose
156/// explicit intermediate paths.
157#[cfg(feature = "session-client")]
158#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault)]
159pub struct HoprSessionClientConfig {
160    /// Forward route selection policy.
161    pub forward_path: HopRouting,
162    /// Return route selection policy.
163    pub return_path: HopRouting,
164    /// Capabilities offered by the session.
165    #[default(_code = "SessionCapability::Segmentation.into()")]
166    pub capabilities: SessionCapabilities,
167    /// Optional pseudonym used for the session. Mostly useful for testing only.
168    #[default(None)]
169    pub pseudonym: Option<hopr_api::types::internal::protocol::HoprPseudonym>,
170    /// Enable automatic SURB management for the session.
171    #[default(Some(SurbBalancerConfig::default()))]
172    pub surb_management: Option<SurbBalancerConfig>,
173    /// If set, the maximum number of possible SURBs will always be sent with session data packets.
174    #[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/// Long-running tasks that are spawned by the HOPR node.
193#[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)] // constructed only with feature = "session-server"
199    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/// Prepare an optimized version of the tokio runtime setup for hopr-lib specifically.
209///
210/// Divide the available CPU parallelism by 2, since half of the available threads are
211/// to be used for IO-bound and half for CPU-bound tasks.
212#[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
254/// Type alias used to send and receive transport data via a running HOPR node.
255pub 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
265/// Time to wait until the node's keybinding appears on-chain
266const NODE_READY_TIMEOUT: Duration = Duration::from_secs(120);
267
268/// HOPR main object providing the entire HOPR node functionality
269///
270/// Instantiating this object creates all processes and objects necessary for
271/// running the HOPR node. Once created, the node can be started using the
272/// `run()` method.
273///
274/// Externally offered API should be enough to perform all necessary tasks
275/// with the HOPR node manually, but it is advised to create such a configuration
276/// that manual interaction is unnecessary.
277///
278/// As such, the `hopr_lib` serves mainly as an integration point into Rust programs.
279pub 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)] // Handles must stay alive to keep background tasks running
289    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    /// Returns a reference to the network graph.
306    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
364// ---------------------------------------------------------------------------
365// Has* accessor trait implementations
366// ---------------------------------------------------------------------------
367
368/// Maps [`Health`] into a [`ComponentStatus`] for a named component.
369fn 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        // Red is returned both for "zero peers" and "network not initialized"
374        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
502// Available only on Relay nodes that specify `TMgr` that implements TicketManagement
503impl<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/// Per-component status report for the HOPR node.
524#[derive(Debug, Clone)]
525pub struct NodeComponentStatuses {
526    /// Overall node lifecycle state.
527    pub node_state: HoprState,
528    /// Chain/blokli connector status.
529    pub chain: ComponentStatus,
530    /// P2P network layer status.
531    pub network: ComponentStatus,
532    /// Transport layer status.
533    pub transport: ComponentStatus,
534}
535
536impl NodeComponentStatuses {
537    /// Worst-case aggregation: the overall status is the worst of any component.
538    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    /// Returns per-component health statuses for the node.
568    ///
569    /// When the node has reached `Running`, the aggregate `node_state` is
570    /// derived from component statuses (Running → Degraded → Failed).
571    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        // Derive aggregate HoprState from component statuses once Running
581        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    /// Prometheus formatted metrics collected by the hopr-lib components.
598    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
727/// Converts a PeerId to an OffchainPublicKey.
728///
729/// This is a standalone utility function, not part of the API traits.
730pub fn peer_id_to_offchain_key(peer_id: &PeerId) -> errors::Result<OffchainPublicKey> {
731    Ok(hopr_transport::peer_id_to_public_key(peer_id)?)
732}