templar_common/
time_chunk.rs

1use near_sdk::{env, json_types::U64, near};
2
3#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4#[near(serializers = [json, borsh])]
5pub enum V0 {
6    BlockHeight { divisor: U64 },
7    EpochHeight { divisor: U64 },
8    BlockTimestampMs { divisor: U64 },
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
12#[near(serializers = [json, borsh])]
13pub struct V1 {
14    pub duration_ms: U64,
15}
16
17/// Configure a method of determining the current time chunk.
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
19#[near(serializers = [json, borsh])]
20#[cfg_attr(not(target_arch = "wasm32"), schemars(untagged))]
21pub enum TimeChunkConfiguration {
22    #[serde(untagged)]
23    V0(V0),
24    #[serde(untagged)]
25    V1(V1),
26}
27
28impl TimeChunkConfiguration {
29    pub fn new(duration_ms: u64) -> Self {
30        Self::V1(V1 {
31            duration_ms: U64(duration_ms),
32        })
33    }
34
35    pub fn duration_ms(&self) -> u64 {
36        match self {
37            TimeChunkConfiguration::V0(V0::BlockTimestampMs { divisor }) => divisor.0,
38            TimeChunkConfiguration::V0(_) => {
39                crate::panic_with_message("Unsupported time chunk configuration")
40            }
41            TimeChunkConfiguration::V1(v1) => v1.duration_ms.0,
42        }
43    }
44
45    pub fn now(&self) -> TimeChunk {
46        let block_timestamp_ms = env::block_timestamp_ms();
47        TimeChunk(U64(block_timestamp_ms
48            .checked_div(self.duration_ms())
49            .unwrap_or(block_timestamp_ms)))
50    }
51
52    pub fn previous(&self) -> TimeChunk {
53        let TimeChunk(U64(time)) = self.now();
54        #[allow(clippy::unwrap_used, reason = "Assume now > 0")]
55        TimeChunk(U64(time.checked_sub(1).unwrap()))
56    }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
60#[near(serializers = [borsh, json])]
61pub struct TimeChunk(pub U64);
62
63#[cfg(test)]
64mod tests {
65    use near_sdk::serde_json;
66    use near_sdk::test_utils;
67
68    use super::*;
69
70    #[test]
71    fn schema_matches_serde_wire_forms() {
72        let schema = near_sdk::serde_json::to_value(schemars::schema_for!(TimeChunkConfiguration))
73            .expect("time chunk schema serializes");
74        let validator = jsonschema::draft7::new(&schema).expect("time chunk schema is Draft 7");
75        let configurations = [
76            (
77                TimeChunkConfiguration::V0(V0::BlockTimestampMs {
78                    divisor: U64(600_000),
79                }),
80                serde_json::json!({"BlockTimestampMs":{"divisor":"600000"}}),
81            ),
82            (
83                TimeChunkConfiguration::V1(V1 {
84                    duration_ms: U64(600_000),
85                }),
86                serde_json::json!({"duration_ms":"600000"}),
87            ),
88        ];
89
90        for (configuration, expected_json) in configurations {
91            let serialized =
92                serde_json::to_value(&configuration).expect("time chunk serialization succeeds");
93            assert_eq!(serialized, expected_json);
94            validator
95                .validate(&serialized)
96                .expect("serialized time chunk is valid under its schema");
97            assert_eq!(
98                serde_json::from_value::<TimeChunkConfiguration>(serialized)
99                    .expect("time chunk deserialization succeeds"),
100                configuration
101            );
102        }
103    }
104
105    #[test]
106    fn now() {
107        let context = test_utils::VMContextBuilder::new()
108            .block_timestamp((600_000 * 45 + 12345) * 1_000_000 /* ms -> ns */)
109            .build();
110        near_sdk::testing_env!(context.clone());
111
112        let t = TimeChunkConfiguration::new(600_000);
113        assert_eq!(t.duration_ms(), 600_000);
114        let now = t.now();
115        assert_eq!(now, TimeChunk(U64(45)));
116        let prev = t.previous();
117        assert_eq!(prev, TimeChunk(U64(44)));
118    }
119
120    #[test]
121    fn v0_deserialization() {
122        let s = r#"{
123          "BlockTimestampMs": {
124            "divisor": "600000"
125          }
126        }"#;
127
128        let d: TimeChunkConfiguration = serde_json::from_str(s).unwrap();
129
130        assert_eq!(
131            d,
132            TimeChunkConfiguration::V0(V0::BlockTimestampMs {
133                divisor: U64(600_000),
134            }),
135        );
136        assert_eq!(d.duration_ms(), 600_000);
137    }
138
139    #[test]
140    fn v0_serialization() {
141        let v0 = TimeChunkConfiguration::V0(V0::BlockTimestampMs {
142            divisor: U64(600_000),
143        });
144
145        let s = serde_json::to_string(&v0).unwrap();
146
147        assert_eq!(s, r#"{"BlockTimestampMs":{"divisor":"600000"}}"#);
148    }
149
150    #[test]
151    fn v1_deserialization_allows_version_metadata() {
152        let configuration: TimeChunkConfiguration =
153            serde_json::from_str(r#"{"version":"V1","duration_ms":"600000"}"#)
154                .expect("V1 accepts ignored version metadata");
155
156        assert_eq!(
157            configuration,
158            TimeChunkConfiguration::V1(V1 {
159                duration_ms: U64(600_000),
160            })
161        );
162    }
163
164    #[test]
165    fn v1_deserialization() {
166        let s = r#"{
167          "duration_ms": "600000"
168        }"#;
169
170        let d: TimeChunkConfiguration = serde_json::from_str(s).unwrap();
171
172        assert_eq!(
173            d,
174            TimeChunkConfiguration::V1(V1 {
175                duration_ms: U64(600_000),
176            }),
177        );
178        assert_eq!(d.duration_ms(), 600_000);
179    }
180
181    #[test]
182    fn v1_serialization() {
183        let v0 = TimeChunkConfiguration::V1(V1 {
184            duration_ms: U64(600_000),
185        });
186
187        let s = serde_json::to_string(&v0).unwrap();
188
189        assert_eq!(s, r#"{"duration_ms":"600000"}"#);
190    }
191}