cw_utils/
expiration.rs

1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{BlockInfo, StdError, StdResult, Timestamp};
3use std::cmp::Ordering;
4use std::fmt;
5use std::ops::{Add, Mul};
6
7/// Expiration represents a point in time when some event happens.
8/// It can compare with a BlockInfo and will return is_expired() == true
9/// once the condition is hit (and for every block in the future)
10#[cw_serde]
11#[derive(Copy)]
12pub enum Expiration {
13    /// AtHeight will expire when `env.block.height` >= height
14    AtHeight(u64),
15    /// AtTime will expire when `env.block.time` >= time
16    AtTime(Timestamp),
17    /// Never will never expire. Used to express the empty variant
18    Never {},
19}
20
21impl fmt::Display for Expiration {
22    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
23        match self {
24            Expiration::AtHeight(height) => write!(f, "expiration height: {}", height),
25            Expiration::AtTime(time) => write!(f, "expiration time: {}", time),
26            Expiration::Never {} => write!(f, "expiration: never"),
27        }
28    }
29}
30
31/// The default (empty value) is to never expire
32impl Default for Expiration {
33    fn default() -> Self {
34        Expiration::Never {}
35    }
36}
37
38impl Expiration {
39    pub fn is_expired(&self, block: &BlockInfo) -> bool {
40        match self {
41            Expiration::AtHeight(height) => block.height >= *height,
42            Expiration::AtTime(time) => block.time >= *time,
43            Expiration::Never {} => false,
44        }
45    }
46}
47
48impl Add<Duration> for Expiration {
49    type Output = StdResult<Expiration>;
50
51    fn add(self, duration: Duration) -> StdResult<Expiration> {
52        match (self, duration) {
53            (Expiration::AtTime(t), Duration::Time(delta)) => {
54                Ok(Expiration::AtTime(t.plus_seconds(delta)))
55            }
56            (Expiration::AtHeight(h), Duration::Height(delta)) => {
57                Ok(Expiration::AtHeight(h + delta))
58            }
59            (Expiration::Never {}, _) => Ok(Expiration::Never {}),
60            _ => Err(StdError::msg("Cannot add height and time")),
61        }
62    }
63}
64
65// TODO: does this make sense? do we get expected info/error when None is returned???
66impl PartialOrd for Expiration {
67    fn partial_cmp(&self, other: &Expiration) -> Option<Ordering> {
68        match (self, other) {
69            // compare if both height or both time
70            (Expiration::AtHeight(h1), Expiration::AtHeight(h2)) => Some(h1.cmp(h2)),
71            (Expiration::AtTime(t1), Expiration::AtTime(t2)) => Some(t1.cmp(t2)),
72            // if at least one is never, we can compare with anything
73            (Expiration::Never {}, Expiration::Never {}) => Some(Ordering::Equal),
74            (Expiration::Never {}, _) => Some(Ordering::Greater),
75            (_, Expiration::Never {}) => Some(Ordering::Less),
76            // if they are mis-matched finite ends, no compare possible
77            _ => None,
78        }
79    }
80}
81
82pub const HOUR: Duration = Duration::Time(60 * 60);
83pub const DAY: Duration = Duration::Time(24 * 60 * 60);
84pub const WEEK: Duration = Duration::Time(7 * 24 * 60 * 60);
85
86/// Duration is a delta of time. You can add it to a BlockInfo or Expiration to
87/// move that further in the future. Note that an height-based Duration and
88/// a time-based Expiration cannot be combined
89#[cw_serde]
90#[derive(Copy)]
91pub enum Duration {
92    Height(u64),
93    /// Time in seconds
94    Time(u64),
95}
96
97impl fmt::Display for Duration {
98    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99        match self {
100            Duration::Height(height) => write!(f, "height: {}", height),
101            Duration::Time(time) => write!(f, "time: {}", time),
102        }
103    }
104}
105
106impl Duration {
107    /// Create an expiration for Duration after current block
108    pub fn after(&self, block: &BlockInfo) -> Expiration {
109        match self {
110            Duration::Height(h) => Expiration::AtHeight(block.height + h),
111            Duration::Time(t) => Expiration::AtTime(block.time.plus_seconds(*t)),
112        }
113    }
114
115    // creates a number just a little bigger, so we can use it to pass expiration point
116    pub fn plus_one(&self) -> Duration {
117        match self {
118            Duration::Height(h) => Duration::Height(h + 1),
119            Duration::Time(t) => Duration::Time(t + 1),
120        }
121    }
122}
123
124impl Add<Duration> for Duration {
125    type Output = StdResult<Duration>;
126
127    fn add(self, rhs: Duration) -> StdResult<Duration> {
128        match (self, rhs) {
129            (Duration::Time(t), Duration::Time(t2)) => Ok(Duration::Time(t + t2)),
130            (Duration::Height(h), Duration::Height(h2)) => Ok(Duration::Height(h + h2)),
131            _ => Err(StdError::msg("Cannot add height and time")),
132        }
133    }
134}
135
136impl Mul<u64> for Duration {
137    type Output = Duration;
138
139    fn mul(self, rhs: u64) -> Self::Output {
140        match self {
141            Duration::Time(t) => Duration::Time(t * rhs),
142            Duration::Height(h) => Duration::Height(h * rhs),
143        }
144    }
145}
146
147#[cfg(test)]
148mod test {
149    use super::*;
150
151    #[test]
152    fn compare_expiration() {
153        // matching pairs
154        assert!(Expiration::AtHeight(5) < Expiration::AtHeight(10));
155        assert!(Expiration::AtHeight(8) > Expiration::AtHeight(7));
156        assert!(
157            Expiration::AtTime(Timestamp::from_seconds(555))
158                < Expiration::AtTime(Timestamp::from_seconds(777))
159        );
160        assert!(
161            Expiration::AtTime(Timestamp::from_seconds(86))
162                < Expiration::AtTime(Timestamp::from_seconds(100))
163        );
164
165        // never as infinity
166        assert!(Expiration::AtHeight(500000) < Expiration::Never {});
167        assert!(Expiration::Never {} > Expiration::AtTime(Timestamp::from_seconds(500000)));
168
169        // what happens for the uncomparables?? all compares are false
170        assert_eq!(
171            None,
172            Expiration::AtTime(Timestamp::from_seconds(1000))
173                .partial_cmp(&Expiration::AtHeight(230))
174        );
175        assert_eq!(
176            Expiration::AtTime(Timestamp::from_seconds(1000))
177                .partial_cmp(&Expiration::AtHeight(230)),
178            None
179        );
180        assert_eq!(
181            Expiration::AtTime(Timestamp::from_seconds(1000))
182                .partial_cmp(&Expiration::AtHeight(230)),
183            None
184        );
185        assert!(!(Expiration::AtTime(Timestamp::from_seconds(1000)) == Expiration::AtHeight(230)));
186    }
187
188    #[test]
189    fn expiration_addition() {
190        // height
191        let end = Expiration::AtHeight(12345) + Duration::Height(400);
192        assert_eq!(end.unwrap(), Expiration::AtHeight(12745));
193
194        // time
195        let end = Expiration::AtTime(Timestamp::from_seconds(55544433)) + Duration::Time(40300);
196        assert_eq!(
197            end.unwrap(),
198            Expiration::AtTime(Timestamp::from_seconds(55584733))
199        );
200
201        // never
202        let end = Expiration::Never {} + Duration::Time(40300);
203        assert_eq!(end.unwrap(), Expiration::Never {});
204
205        // mismatched
206        let end = Expiration::AtHeight(12345) + Duration::Time(1500);
207        end.unwrap_err();
208
209        // // not possible other way
210        // let end = Duration::Time(1000) + Expiration::AtTime(50000);
211        // assert_eq!(end.unwrap(), Expiration::AtTime(51000));
212    }
213
214    #[test]
215    fn block_plus_duration() {
216        let block = BlockInfo {
217            height: 1000,
218            time: Timestamp::from_seconds(7777),
219            chain_id: "foo".to_string(),
220        };
221
222        let end = Duration::Height(456).after(&block);
223        assert_eq!(Expiration::AtHeight(1456), end);
224
225        let end = Duration::Time(1212).after(&block);
226        assert_eq!(Expiration::AtTime(Timestamp::from_seconds(8989)), end);
227    }
228
229    #[test]
230    fn duration_math() {
231        let long = (Duration::Height(444) + Duration::Height(555)).unwrap();
232        assert_eq!(Duration::Height(999), long);
233
234        let days = DAY * 3;
235        assert_eq!(Duration::Time(3 * 24 * 60 * 60), days);
236    }
237}