ibc_types_timestamp/
lib.rs

1//! A timestamp type for IBC.
2#![no_std]
3// Requires nightly.
4#![cfg_attr(docsrs, feature(doc_auto_cfg))]
5
6extern crate alloc;
7#[cfg(any(test, feature = "std"))]
8extern crate std;
9
10mod prelude;
11use prelude::*;
12
13use core::fmt::{Display, Error as FmtError, Formatter};
14use core::hash::{Hash, Hasher};
15use core::num::ParseIntError;
16use core::ops::{Add, Sub};
17use core::str::FromStr;
18use core::time::Duration;
19
20use displaydoc::Display;
21use tendermint::Time;
22use time::OffsetDateTime;
23
24pub const ZERO_DURATION: Duration = Duration::from_secs(0);
25
26/// A newtype wrapper over `Option<Time>` to keep track of
27/// IBC packet timeout.
28///
29/// We use an explicit `Option` type to distinguish this when converting between
30/// a `u64` value and a raw timestamp. In protocol buffer, the timestamp is
31/// represented as a `u64` Unix timestamp in nanoseconds, with 0 representing the absence
32/// of timestamp.
33#[cfg_attr(feature = "with_serde", derive(serde::Serialize, serde::Deserialize))]
34#[derive(PartialEq, Eq, Copy, Clone, Debug, Default)]
35pub struct Timestamp {
36    pub time: Option<Time>,
37}
38
39// TODO: derive when tendermint::Time supports it:
40// https://github.com/informalsystems/tendermint-rs/pull/1054
41#[allow(clippy::derived_hash_with_manual_eq)]
42impl Hash for Timestamp {
43    fn hash<H: Hasher>(&self, state: &mut H) {
44        let odt: Option<OffsetDateTime> = self.time.map(Into::into);
45        odt.hash(state);
46    }
47}
48
49/// The expiry result when comparing two timestamps.
50/// - If either timestamp is invalid (0), the result is `InvalidTimestamp`.
51/// - If the left timestamp is strictly after the right timestamp, the result is `Expired`.
52/// - Otherwise, the result is `NotExpired`.
53///
54/// User of this result may want to determine whether error should be raised,
55/// when either of the timestamp being compared is invalid.
56#[cfg_attr(feature = "with_serde", derive(serde::Serialize, serde::Deserialize))]
57#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)]
58pub enum Expiry {
59    Expired,
60    NotExpired,
61    InvalidTimestamp,
62}
63
64impl Timestamp {
65    /// The IBC protocol represents timestamps as u64 Unix
66    /// timestamps in nanoseconds.
67    ///
68    /// A protocol value of 0 indicates that the timestamp
69    /// is not set. In this case, our domain type takes the
70    /// value of None.
71    ///
72    pub fn from_nanoseconds(nanoseconds: u64) -> Result<Timestamp, ParseTimestampError> {
73        if nanoseconds == 0 {
74            Ok(Timestamp { time: None })
75        } else {
76            // As the `u64` representation can only represent times up to
77            // about year 2554, there is no risk of overflowing `Time`
78            // or `OffsetDateTime`.
79            let ts = OffsetDateTime::from_unix_timestamp_nanos(nanoseconds as i128)
80                .unwrap()
81                .try_into()
82                .unwrap();
83            Ok(Timestamp { time: Some(ts) })
84        }
85    }
86
87    /// Returns a `Timestamp` representation of the current time.
88    #[cfg(feature = "std")]
89    pub fn now() -> Timestamp {
90        Time::now().into()
91    }
92
93    /// Returns a `Timestamp` representation of a timestamp not being set.
94    pub fn none() -> Self {
95        Timestamp { time: None }
96    }
97
98    /// Computes the duration difference of another `Timestamp` from the current one.
99    /// Returns the difference in time as an [`core::time::Duration`].
100    /// Returns `None` if the other `Timestamp` is more advanced
101    /// than the current or if either of the `Timestamp`s is not set.
102    pub fn duration_since(&self, other: &Timestamp) -> Option<Duration> {
103        match (self.time, other.time) {
104            (Some(time1), Some(time2)) => time1.duration_since(time2).ok(),
105            _ => None,
106        }
107    }
108
109    /// Convert a `Timestamp` to `u64` value in nanoseconds. If no timestamp
110    /// is set, the result is 0.
111    ///
112    #[deprecated(since = "0.9.1", note = "use `nanoseconds` instead")]
113    pub fn as_nanoseconds(&self) -> u64 {
114        (*self).nanoseconds()
115    }
116
117    /// Convert a `Timestamp` to `u64` value in nanoseconds. If no timestamp
118    /// is set, the result is 0.
119    /// ```
120    /// # use ibc_types_timestamp::Timestamp;
121    /// let max = u64::MAX;
122    /// let tx = Timestamp::from_nanoseconds(max).unwrap();
123    /// let utx = tx.nanoseconds();
124    /// assert_eq!(utx, max);
125    /// let min = u64::MIN;
126    /// let ti = Timestamp::from_nanoseconds(min).unwrap();
127    /// let uti = ti.nanoseconds();
128    /// assert_eq!(uti, min);
129    /// let tz = Timestamp::default();
130    /// let utz = tz.nanoseconds();
131    /// assert_eq!(utz, 0);
132    /// ```
133    pub fn nanoseconds(self) -> u64 {
134        self.time.map_or(0, |time| {
135            let t: OffsetDateTime = time.into();
136            let s = t.unix_timestamp_nanos();
137            assert!(s >= 0, "time {time:?} has negative `.timestamp()`");
138            s.try_into().unwrap()
139        })
140    }
141
142    /// Convert a `Timestamp` to an optional [`OffsetDateTime`]
143    pub fn into_datetime(self) -> Option<OffsetDateTime> {
144        self.time.map(Into::into)
145    }
146
147    /// Convert a `Timestamp` to an optional [`tendermint::Time`]
148    pub fn into_tm_time(self) -> Option<Time> {
149        self.time
150    }
151
152    /// Checks whether the timestamp has expired when compared to the
153    /// `other` timestamp. Returns an [`Expiry`] result.
154    pub fn check_expiry(&self, other: &Timestamp) -> Expiry {
155        match (self.time, other.time) {
156            (Some(time1), Some(time2)) => {
157                if time1 > time2 {
158                    Expiry::Expired
159                } else {
160                    Expiry::NotExpired
161                }
162            }
163            _ => Expiry::InvalidTimestamp,
164        }
165    }
166
167    /// Checks whether the current timestamp is strictly more advanced
168    /// than the `other` timestamp. Return true if so, and false
169    /// otherwise.
170    pub fn after(&self, other: &Timestamp) -> bool {
171        match (self.time, other.time) {
172            (Some(time1), Some(time2)) => time1 > time2,
173            _ => false,
174        }
175    }
176}
177
178// TODO BUG : this must round trip with fromstr
179impl Display for Timestamp {
180    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
181        write!(
182            f,
183            "Timestamp({})",
184            self.time
185                .map_or("NoTimestamp".to_string(), |time| time.to_rfc3339())
186        )
187    }
188}
189
190#[derive(Debug, Display)]
191pub enum TimestampOverflowError {
192    /// Timestamp overflow when modifying with duration
193    TimestampOverflow,
194}
195
196#[cfg(feature = "std")]
197impl std::error::Error for TimestampOverflowError {}
198
199impl Add<Duration> for Timestamp {
200    type Output = Result<Timestamp, TimestampOverflowError>;
201
202    fn add(self, duration: Duration) -> Result<Timestamp, TimestampOverflowError> {
203        match self.time {
204            Some(time) => {
205                let time =
206                    (time + duration).map_err(|_| TimestampOverflowError::TimestampOverflow)?;
207                Ok(Timestamp { time: Some(time) })
208            }
209            None => Ok(self),
210        }
211    }
212}
213
214impl Sub<Duration> for Timestamp {
215    type Output = Result<Timestamp, TimestampOverflowError>;
216
217    fn sub(self, duration: Duration) -> Result<Timestamp, TimestampOverflowError> {
218        match self.time {
219            Some(time) => {
220                let time =
221                    (time - duration).map_err(|_| TimestampOverflowError::TimestampOverflow)?;
222                Ok(Timestamp { time: Some(time) })
223            }
224            None => Ok(self),
225        }
226    }
227}
228
229#[derive(Debug, Display)]
230pub enum ParseTimestampError {
231    /// parsing u64 integer from string error: `{0}`
232    ParseInt(ParseIntError),
233}
234
235#[cfg(feature = "std")]
236impl std::error::Error for ParseTimestampError {
237    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
238        match &self {
239            ParseTimestampError::ParseInt(e) => Some(e),
240        }
241    }
242}
243
244impl FromStr for Timestamp {
245    type Err = ParseTimestampError;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        let nanoseconds = u64::from_str(s).map_err(ParseTimestampError::ParseInt)?;
249
250        Timestamp::from_nanoseconds(nanoseconds)
251    }
252}
253
254impl From<Time> for Timestamp {
255    fn from(tendermint_time: Time) -> Timestamp {
256        Timestamp {
257            time: Some(tendermint_time),
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use time::OffsetDateTime;
265
266    use core::time::Duration;
267    use std::thread::sleep;
268    use test_log::test;
269
270    use super::{Expiry, Timestamp, ZERO_DURATION};
271
272    #[test]
273    fn test_timestamp_comparisons() {
274        let nil_timestamp = Timestamp::from_nanoseconds(0).unwrap();
275        assert_eq!(nil_timestamp.time, None);
276        assert_eq!(nil_timestamp.nanoseconds(), 0);
277
278        let timestamp1 = Timestamp::from_nanoseconds(1).unwrap();
279        let dt: OffsetDateTime = timestamp1.time.unwrap().into();
280        assert_eq!(dt.unix_timestamp_nanos(), 1);
281        assert_eq!(timestamp1.nanoseconds(), 1);
282
283        let timestamp2 = Timestamp::from_nanoseconds(1_000_000_000).unwrap();
284        let dt: OffsetDateTime = timestamp2.time.unwrap().into();
285        assert_eq!(dt.unix_timestamp_nanos(), 1_000_000_000);
286        assert_eq!(timestamp2.nanoseconds(), 1_000_000_000);
287
288        assert!(Timestamp::from_nanoseconds(u64::MAX).is_ok());
289        assert!(Timestamp::from_nanoseconds(i64::MAX.try_into().unwrap()).is_ok());
290
291        assert_eq!(timestamp1.check_expiry(&timestamp2), Expiry::NotExpired);
292        assert_eq!(timestamp1.check_expiry(&timestamp1), Expiry::NotExpired);
293        assert_eq!(timestamp2.check_expiry(&timestamp2), Expiry::NotExpired);
294        assert_eq!(timestamp2.check_expiry(&timestamp1), Expiry::Expired);
295        assert_eq!(
296            timestamp1.check_expiry(&nil_timestamp),
297            Expiry::InvalidTimestamp
298        );
299        assert_eq!(
300            nil_timestamp.check_expiry(&timestamp2),
301            Expiry::InvalidTimestamp
302        );
303        assert_eq!(
304            nil_timestamp.check_expiry(&nil_timestamp),
305            Expiry::InvalidTimestamp
306        );
307    }
308
309    #[test]
310    fn test_timestamp_arithmetic() {
311        let time0 = Timestamp::none();
312        let time1 = Timestamp::from_nanoseconds(100).unwrap();
313        let time2 = Timestamp::from_nanoseconds(150).unwrap();
314        let time3 = Timestamp::from_nanoseconds(50).unwrap();
315        let duration = Duration::from_nanos(50);
316
317        assert_eq!(time1, (time1 + ZERO_DURATION).unwrap());
318        assert_eq!(time2, (time1 + duration).unwrap());
319        assert_eq!(time3, (time1 - duration).unwrap());
320        assert_eq!(time0, (time0 + duration).unwrap());
321        assert_eq!(time0, (time0 - duration).unwrap());
322    }
323
324    #[test]
325    fn subtract_compare() {
326        let sleep_duration = Duration::from_micros(100);
327
328        let start = Timestamp::now();
329        sleep(sleep_duration);
330        let end = Timestamp::now();
331
332        let res = end.duration_since(&start);
333        assert!(res.is_some());
334
335        let inner = res.unwrap();
336        assert!(inner > sleep_duration);
337    }
338}