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#[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/// Session client configuration for `hopr-lib`.
126///
127/// Unlike transport-level configuration, this API intentionally does not expose
128/// explicit intermediate paths.
129#[cfg(feature = "session-client")]
130#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault)]
131pub struct HoprSessionClientConfig {
132    /// Forward route selection policy.
133    pub forward_path: HopRouting,
134    /// Return route selection policy.
135    pub return_path: HopRouting,
136    /// Capabilities offered by the session.
137    #[default(_code = "SessionCapability::Segmentation.into()")]
138    pub capabilities: SessionCapabilities,
139    /// Optional pseudonym used for the session. Mostly useful for testing only.
140    #[default(None)]
141    pub pseudonym: Option<hopr_api::types::internal::protocol::HoprPseudonym>,
142    /// Enable automatic SURB management for the session.
143    #[default(Some(SurbBalancerConfig::default()))]
144    pub surb_management: Option<SurbBalancerConfig>,
145    /// If set, the maximum number of possible SURBs will always be sent with session data packets.
146    #[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/// Long-running tasks that are spawned by the HOPR node.
165#[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)] // constructed only with feature = "session-server"
171    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/// Prepare an optimized version of the tokio runtime setup for hopr-lib specifically.
181///
182/// Divide the available CPU parallelism by 2, since half of the available threads are
183/// to be used for IO-bound and half for CPU-bound tasks.
184#[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
226/// Type alias used to send and receive transport data via a running HOPR node.
227pub 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
237/// Time to wait until the node's keybinding appears on-chain
238const NODE_READY_TIMEOUT: Duration = Duration::from_secs(120);
239
240/// HOPR main object providing the entire HOPR node functionality
241///
242/// Instantiating this object creates all processes and objects necessary for
243/// running the HOPR node. Once created, the node can be started using the
244/// `run()` method.
245///
246/// Externally offered API should be enough to perform all necessary tasks
247/// with the HOPR node manually, but it is advised to create such a configuration
248/// that manual interaction is unnecessary.
249///
250/// As such, the `hopr_lib` serves mainly as an integration point into Rust programs.
251pub 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)] // Handles must stay alive to keep background tasks running
261    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    /// Returns a reference to the network graph.
289    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
347// ---------------------------------------------------------------------------
348// Has* accessor trait implementations
349// ---------------------------------------------------------------------------
350
351/// Maps [`Health`] into a [`ComponentStatus`] for a named component.
352fn 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        // Red is returned both for "zero peers" and "network not initialized"
357        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
493// Available only on Relay nodes that specify `TMgr` that implements TicketManagement
494impl<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        // `Merge` provides fair polling distribution across active sources.
563        Ok(streams.merge().boxed())
564    }
565}
566
567/// Per-component status report for the HOPR node.
568#[derive(Debug, Clone)]
569pub struct NodeComponentStatuses {
570    /// Overall node lifecycle state.
571    pub node_state: HoprState,
572    /// Chain/blokli connector status.
573    pub chain: ComponentStatus,
574    /// P2P network layer status.
575    pub network: ComponentStatus,
576    /// Transport layer status.
577    pub transport: ComponentStatus,
578}
579
580impl NodeComponentStatuses {
581    /// Worst-case aggregation: the overall status is the worst of any component.
582    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    /// Returns per-component health statuses for the node.
612    ///
613    /// When the node has reached `Running`, the aggregate `node_state` is
614    /// derived from component statuses (Running → Degraded → Failed).
615    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        // Derive aggregate HoprState from component statuses once Running
625        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/// Prometheus-formatted metrics collected by the hopr-lib components.
647///
648/// Only available when compiled with the `telemetry` feature.
649#[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
766/// Converts a PeerId to an OffchainPublicKey.
767///
768/// This is a standalone utility function, not part of the API traits.
769pub fn peer_id_to_offchain_key(peer_id: &PeerId) -> errors::Result<OffchainPublicKey> {
770    Ok(hopr_transport::peer_id_to_public_key(peer_id)?)
771}