mithril_ticker/
ticker_service.rs

1//! ## Ticker Service
2//!
3//! This service read time information from the chain and helps create beacons
4//! for every message types.
5
6use anyhow::Context;
7use async_trait::async_trait;
8use std::sync::Arc;
9use thiserror::Error;
10
11use mithril_cardano_node_chain::chain_observer::ChainObserver;
12use mithril_cardano_node_internal_database::ImmutableFileObserver;
13use mithril_common::StdResult;
14use mithril_common::entities::{Epoch, TimePoint};
15
16/// ## TickerService
17///
18/// This service is responsible for giving the right time information to other
19/// services. It reads data either from the Chain or the filesystem to create
20/// beacons for each message type.
21#[async_trait]
22pub trait TickerService
23where
24    Self: Sync + Send,
25{
26    /// Get the current [Epoch] of the cardano node.
27    async fn get_current_epoch(&self) -> StdResult<Epoch> {
28        self.get_current_time_point().await.map(|time_point| time_point.epoch)
29    }
30
31    /// Get the current [TimePoint] of the cardano node.
32    async fn get_current_time_point(&self) -> StdResult<TimePoint>;
33}
34
35/// [TickerService] related errors.
36#[derive(Error, Debug)]
37pub enum TickerServiceError {
38    /// Raised when reading the current epoch succeeded but yielded no result.
39    #[error("No epoch yielded by the chain observer, is your cardano node ready?")]
40    NoEpoch,
41
42    /// Raised when reading the current chain point succeeded but yielded no result.
43    #[error("No chain point yielded by the chain observer, is your cardano node ready?")]
44    NoChainPoint,
45}
46
47/// A [TickerService] using a [ChainObserver] and a [ImmutableFileObserver].
48pub struct MithrilTickerService {
49    chain_observer: Arc<dyn ChainObserver>,
50    immutable_observer: Arc<dyn ImmutableFileObserver>,
51}
52
53impl MithrilTickerService {
54    /// [MithrilTickerService] factory.
55    pub fn new(
56        chain_observer: Arc<dyn ChainObserver>,
57        immutable_observer: Arc<dyn ImmutableFileObserver>,
58    ) -> Self {
59        Self {
60            chain_observer,
61            immutable_observer,
62        }
63    }
64}
65
66#[async_trait]
67impl TickerService for MithrilTickerService {
68    async fn get_current_time_point(&self) -> StdResult<TimePoint> {
69        let epoch = self
70            .chain_observer
71            .get_current_epoch()
72            .await
73            .with_context(|| "TimePoint Provider can not get current epoch")?
74            .ok_or(TickerServiceError::NoEpoch)?;
75
76        let immutable_file_number = self
77            .immutable_observer
78            .get_last_immutable_number()
79            .await
80            .with_context(|| {
81                format!(
82                    "TimePoint Provider can not get last immutable file number for epoch: '{epoch}'"
83                )
84            })?;
85
86        let chain_point = self
87            .chain_observer
88            .get_current_chain_point()
89            .await
90            .with_context(|| "TimePoint Provider can not get current chain point")?
91            .ok_or(TickerServiceError::NoChainPoint)?;
92
93        Ok(TimePoint {
94            epoch,
95            immutable_file_number,
96            chain_point,
97        })
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use anyhow::anyhow;
104
105    use mithril_cardano_node_chain::chain_observer::{ChainObserver, ChainObserverError};
106    use mithril_cardano_node_chain::entities::{ChainAddress, TxDatum};
107    use mithril_cardano_node_internal_database::test::double::DumbImmutableFileObserver;
108    use mithril_common::entities::{BlockNumber, ChainPoint, Epoch, SlotNumber, StakeDistribution};
109
110    use super::*;
111
112    struct DumbChainObserver {}
113
114    #[async_trait]
115    impl ChainObserver for DumbChainObserver {
116        async fn get_current_datums(
117            &self,
118            _address: &ChainAddress,
119        ) -> Result<Vec<TxDatum>, ChainObserverError> {
120            Ok(Vec::new())
121        }
122
123        async fn get_current_era(&self) -> Result<Option<String>, ChainObserverError> {
124            Ok(Some(String::new()))
125        }
126
127        async fn get_current_epoch(&self) -> Result<Option<Epoch>, ChainObserverError> {
128            Ok(Some(Epoch(42)))
129        }
130
131        async fn get_current_chain_point(&self) -> Result<Option<ChainPoint>, ChainObserverError> {
132            Ok(Some(ChainPoint {
133                slot_number: SlotNumber(800),
134                block_number: BlockNumber(51),
135                block_hash: "1b69b3202fbe500".to_string(),
136            }))
137        }
138
139        async fn get_current_stake_distribution(
140            &self,
141        ) -> Result<Option<StakeDistribution>, ChainObserverError> {
142            Err(ChainObserverError::General(anyhow!(
143                "this should not be called in the TimePointProvider"
144            )))
145        }
146    }
147
148    #[tokio::test]
149    async fn test_get_current_epoch() {
150        let ticker_service = MithrilTickerService::new(
151            Arc::new(DumbChainObserver {}),
152            Arc::new(DumbImmutableFileObserver::default()),
153        );
154        let epoch = ticker_service.get_current_epoch().await.unwrap();
155
156        assert_eq!(Epoch(42), epoch);
157    }
158
159    #[tokio::test]
160    async fn test_happy_path() {
161        let ticker_service = MithrilTickerService::new(
162            Arc::new(DumbChainObserver {}),
163            Arc::new(DumbImmutableFileObserver::default()),
164        );
165        let time_point = ticker_service.get_current_time_point().await.unwrap();
166
167        assert_eq!(
168            TimePoint::new(
169                42,
170                500,
171                ChainPoint {
172                    slot_number: SlotNumber(800),
173                    block_number: BlockNumber(51),
174                    block_hash: "1b69b3202fbe500".to_string(),
175                },
176            ),
177            time_point
178        );
179    }
180
181    #[tokio::test]
182    async fn test_error_from_dependency() {
183        let immutable_observer = DumbImmutableFileObserver::default();
184        immutable_observer.shall_return(None).await;
185        let ticker_service =
186            MithrilTickerService::new(Arc::new(DumbChainObserver {}), Arc::new(immutable_observer));
187
188        let result = ticker_service.get_current_time_point().await;
189        assert!(result.is_err());
190    }
191}