snapshots_math/
lib.rs

1//! Calculations for voting escrow snapshots.
2//!
3//! These functions are split into a separate crate to ensure `anchor-lang` version mismatches
4//! do not prevent building against this code.
5#![deny(rustdoc::all)]
6#![allow(rustdoc::missing_doc_code_examples)]
7#![deny(clippy::unwrap_used, clippy::integer_arithmetic)]
8#![deny(missing_docs)]
9
10use num_traits::cast::ToPrimitive;
11
12/// Number of periods in an era.
13pub const ERA_NUM_PERIODS: usize = (u8::MAX as usize) + 1;
14
15/// Number of seconds in a period.
16pub const PERIOD_SECONDS: u32 = 86_400 * 3;
17
18/// Number of seconds in an era.
19pub const SECONDS_PER_ERA: u64 = (ERA_NUM_PERIODS as u64) * (PERIOD_SECONDS as u64);
20
21/// The Unix timestamp of the start of the first era.
22pub const COMMON_ERA_UNIX_TS: u64 = 1640995200;
23
24/// Calculates the start timestamp of an era.
25pub fn calculate_era_start_ts(era: u16) -> Option<u64> {
26    COMMON_ERA_UNIX_TS.checked_add(SECONDS_PER_ERA.checked_mul(era.into())?)
27}
28
29/// Calculates the start timestamp of a period of an era.
30pub fn calculate_period_start_ts(era: u16, period: u8) -> Option<u64> {
31    calculate_era_start_ts(era)?
32        .checked_add(period.to_u64()?.checked_mul(PERIOD_SECONDS.to_u64()?)?)
33}
34
35/// A period is `elapsed` if its start time has passed.
36///
37/// Elapsed periods cannot increase their locker veToken balance.
38pub fn has_period_elapsed(era: u16, period: u8, now: i64) -> Option<bool> {
39    let start = calculate_period_start_ts(era, period)?;
40    let now = now.to_u64()?;
41    // `>` instead of `>=` to prevent potential off-by-one errors
42    // by programmers that are not aware of the definition of elapsed.
43    // one second isn't a big deal.
44    Some(now > start)
45}
46
47/// Calculates the era and period of the given Unix timestamp.
48pub fn calculate_era_and_period_of_ts(now: u64) -> Option<(u16, u8)> {
49    let current_era: u16 = now
50        .checked_sub(COMMON_ERA_UNIX_TS)?
51        .checked_div(SECONDS_PER_ERA)?
52        .to_u16()?;
53    let current_era_start_ts = calculate_era_start_ts(current_era)?;
54    let current_period: u8 = now
55        .checked_sub(current_era_start_ts)?
56        .checked_div(PERIOD_SECONDS.into())?
57        .to_u8()?;
58    Some((current_era, current_period))
59}
60
61/// Calculates the next era and period of the given period.
62pub fn calculate_next_era_and_period(era: u16, period: u8) -> Option<(u16, u8)> {
63    Some(if period == u8::MAX {
64        (era.checked_add(1)?, 0_u8)
65    } else {
66        (era, period.checked_add(1)?)
67    })
68}
69
70/// Calculates the next era and period of the given Unix timestamp.
71pub fn calculate_next_era_and_period_of_ts(now: u64) -> Option<(u16, u8)> {
72    let (current_era, current_period) = calculate_era_and_period_of_ts(now)?;
73    calculate_next_era_and_period(current_era, current_period)
74}
75
76#[cfg(test)]
77#[allow(clippy::unwrap_used, clippy::integer_arithmetic)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_has_period_elapsed() {
83        // beginning of period 2: so period 2 has not elapsed yet.
84        let current_time = (COMMON_ERA_UNIX_TS + (PERIOD_SECONDS as u64) * 2)
85            .to_i64()
86            .unwrap();
87
88        assert!(has_period_elapsed(0, 0, current_time).unwrap());
89        assert!(has_period_elapsed(0, 1, current_time).unwrap());
90        assert!(!has_period_elapsed(0, 2, current_time).unwrap());
91
92        assert!(!has_period_elapsed(1, 0, current_time).unwrap());
93    }
94
95    #[test]
96    fn test_has_period_elapsed_boundary() {
97        // beginning of period 2: so period 2 has not elapsed yet.
98        let current_time = (COMMON_ERA_UNIX_TS + (PERIOD_SECONDS as u64) * 2 + 1)
99            .to_i64()
100            .unwrap();
101
102        assert!(has_period_elapsed(0, 0, current_time).unwrap());
103        assert!(has_period_elapsed(0, 1, current_time).unwrap());
104        assert!(has_period_elapsed(0, 2, current_time).unwrap());
105        assert!(!has_period_elapsed(0, 3, current_time).unwrap());
106
107        assert!(!has_period_elapsed(1, 0, current_time).unwrap());
108    }
109
110    #[test]
111    fn test_calculate_next_era_and_period_normal() {
112        let era = 2_u16;
113        let period = 4_u8;
114        let start = calculate_period_start_ts(era, period).unwrap() + 40;
115
116        let (result_era, result_period) = calculate_era_and_period_of_ts(start).unwrap();
117        assert_eq!(result_era, era);
118        assert_eq!(result_period, period);
119
120        let (result_next_era, result_next_period) =
121            calculate_next_era_and_period_of_ts(start).unwrap();
122        assert_eq!(result_next_era, era);
123        assert_eq!(result_next_period, period + 1);
124    }
125
126    #[test]
127    fn test_calculate_next_era_and_period_boundary() {
128        let era = 2_u16;
129        let period = 255_u8;
130        let start = calculate_period_start_ts(era, period).unwrap() + 40;
131
132        let (result_era, result_period) = calculate_era_and_period_of_ts(start).unwrap();
133        assert_eq!(result_era, era);
134        assert_eq!(result_period, period);
135
136        let (result_next_era, result_next_period) =
137            calculate_next_era_and_period_of_ts(start).unwrap();
138        assert_eq!(result_next_era, era + 1);
139        assert_eq!(result_next_period, 0_u8);
140    }
141}