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/// Utility module with helper types and functionality over hopr-lib behavior.
27pub mod utils;
28
29pub use hopr_api as api;
30
31/// Exports of libraries necessary for API and interface operations.
32///
33/// Use `hopr_lib::api::types::*` for all type access.
34/// This module retains transport and network-specific types not available in `hopr_lib::api`.
35#[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/// Public routing configuration for session opening in `hopr-lib`.
79///
80/// This intentionally exposes only hop-count based routing.
81#[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    /// Maximum number of hops that can be configured.
94    pub const MAX_HOPS: usize = hopr_api::types::internal::routing::RoutingOptions::MAX_INTERMEDIATE_HOPS;
95
96    /// Returns the configured number of hops.
97    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/// Session client configuration for `hopr-lib`.
119///
120/// Unlike transport-level configuration, this API intentionally does not expose
121/// explicit intermediate paths.
122#[cfg(feature = "session-client")]
123#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault)]
124pub struct HoprSessionClientConfig {
125    /// Forward route selection policy.
126    pub forward_path: HopRouting,
127    /// Return route selection policy.
128    pub return_path: HopRouting,
129    /// Capabilities offered by the session.
130    #[default(_code = "SessionCapability::Segmentation.into()")]
131    pub capabilities: SessionCapabilities,
132    /// Optional pseudonym used for the session. Mostly useful for testing only.
133    #[default(None)]
134    pub pseudonym: Option<hopr_api::types::internal::protocol::HoprPseudonym>,
135    /// Enable automatic SURB management for the session.
136    #[default(Some(SurbBalancerConfig::default()))]
137    pub surb_management: Option<SurbBalancerConfig>,
138    /// If set, the maximum number of possible SURBs will always be sent with session data packets.
139    #[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/// Long-running tasks that are spawned by the HOPR node.
158#[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)] // constructed only with feature = "session-server"
164    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/// Prepare an optimized version of the tokio runtime setup for hopr-lib specifically.
174///
175/// Divide the available CPU parallelism by 2, since half of the available threads are
176/// to be used for IO-bound and half for CPU-bound tasks.
177#[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
219/// Type alias used to send and receive transport data via a running HOPR node.
220pub 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
230/// Time to wait until the node's keybinding appears on-chain
231const NODE_READY_TIMEOUT: Duration = Duration::from_secs(120);
232
233/// HOPR main object providing the entire HOPR node functionality
234///
235/// Instantiating this object creates all processes and objects necessary for
236/// running the HOPR node. Once created, the node can be started using the
237/// `run()` method.
238///
239/// Externally offered API should be enough to perform all necessary tasks
240/// with the HOPR node manually, but it is advised to create such a configuration
241/// that manual interaction is unnecessary.
242///
243/// As such, the `hopr_lib` serves mainly as an integration point into Rust programs.
244pub 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)] // Handles must stay alive to keep background tasks running
254    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    /// Returns a reference to the network graph.
271    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
329// ---------------------------------------------------------------------------
330// Has* accessor trait implementations
331// ---------------------------------------------------------------------------
332
333/// Maps [`Health`] into a [`ComponentStatus`] for a named component.
334fn 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        // Red is returned both for "zero peers" and "network not initialized"
339        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
475// Available only on Relay nodes that specify `TMgr` that implements TicketManagement
476impl<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        // `Merge` provides fair polling distribution across active sources.
545        Ok(streams.merge().boxed())
546    }
547}
548
549/// Per-component status report for the HOPR node.
550#[derive(Debug, Clone)]
551pub struct NodeComponentStatuses {
552    /// Overall node lifecycle state.
553    pub node_state: HoprState,
554    /// Chain/blokli connector status.
555    pub chain: ComponentStatus,
556    /// P2P network layer status.
557    pub network: ComponentStatus,
558    /// Transport layer status.
559    pub transport: ComponentStatus,
560}
561
562impl NodeComponentStatuses {
563    /// Worst-case aggregation: the overall status is the worst of any component.
564    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    /// Returns per-component health statuses for the node.
594    ///
595    /// When the node has reached `Running`, the aggregate `node_state` is
596    /// derived from component statuses (Running → Degraded → Failed).
597    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        // Derive aggregate HoprState from component statuses once Running
607        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    /// Prometheus formatted metrics collected by the hopr-lib components.
624    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
753/// Converts a PeerId to an OffchainPublicKey.
754///
755/// This is a standalone utility function, not part of the API traits.
756pub fn peer_id_to_offchain_key(peer_id: &PeerId) -> errors::Result<OffchainPublicKey> {
757    Ok(hopr_transport::peer_id_to_public_key(peer_id)?)
758}