1use std::fmt::{Display, Formatter};
2use std::num::TryFromIntError;
3use std::ops::{Deref, DerefMut};
4use std::str::FromStr;
5
6use anyhow::Context;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::entities::arithmetic_operation_wrapper::{
11 impl_add_to_wrapper, impl_partial_eq_to_wrapper, impl_sub_to_wrapper,
12};
13use crate::{StdError, StdResult};
14
15const INVALID_EPOCH_SPECIFIER_ERROR: &str =
16 "Invalid epoch: expected 'X', 'latest' or 'latest-X' where X is a positive 64-bit integer";
17
18#[derive(
20 Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize, Hash, Eq, PartialOrd, Ord,
21)]
22pub struct Epoch(pub u64);
23
24impl Epoch {
25 pub const SIGNER_RETRIEVAL_OFFSET: i64 = -1;
27
28 pub const NEXT_SIGNER_RETRIEVAL_OFFSET: u64 = 0;
31
32 pub const SIGNER_RECORDING_OFFSET: u64 = 1;
34
35 pub const EPOCH_SETTINGS_RECORDING_OFFSET: u64 = 2;
37
38 pub const SIGNER_SIGNING_OFFSET: u64 = 2;
41
42 pub const CARDANO_STAKE_DISTRIBUTION_SNAPSHOT_OFFSET: u64 = 2;
45
46 pub const SIGNER_LEADER_SYNCHRONIZATION_OFFSET: u64 = 0;
48
49 pub fn offset_by(&self, epoch_offset: i64) -> Result<Self, EpochError> {
53 let epoch_new = self.0 as i64 + epoch_offset;
54 if epoch_new < 0 {
55 return Err(EpochError::EpochOffset(self.0, epoch_offset));
56 }
57 Ok(Epoch(epoch_new as u64))
58 }
59
60 pub fn offset_to_signer_retrieval_epoch(&self) -> Result<Self, EpochError> {
62 self.offset_by(Self::SIGNER_RETRIEVAL_OFFSET)
63 }
64
65 pub fn offset_to_next_signer_retrieval_epoch(&self) -> Self {
67 *self + Self::NEXT_SIGNER_RETRIEVAL_OFFSET
68 }
69
70 pub fn offset_to_recording_epoch(&self) -> Self {
72 *self + Self::SIGNER_RECORDING_OFFSET
73 }
74
75 pub fn offset_to_epoch_settings_recording_epoch(&self) -> Self {
77 *self + Self::EPOCH_SETTINGS_RECORDING_OFFSET
78 }
79
80 pub fn offset_to_signer_signing_offset(&self) -> Self {
82 *self + Self::SIGNER_SIGNING_OFFSET
83 }
84
85 pub fn offset_to_cardano_stake_distribution_snapshot_epoch(&self) -> Self {
87 *self + Self::CARDANO_STAKE_DISTRIBUTION_SNAPSHOT_OFFSET
88 }
89
90 pub fn offset_to_leader_synchronization_epoch(&self) -> Self {
92 *self + Self::SIGNER_LEADER_SYNCHRONIZATION_OFFSET
93 }
94
95 pub fn next(&self) -> Self {
97 *self + 1
98 }
99
100 pub fn previous(&self) -> Result<Self, EpochError> {
102 self.offset_by(-1)
103 }
104
105 pub fn has_gap_with(&self, other: &Epoch) -> bool {
107 self.0.abs_diff(other.0) > 1
108 }
109}
110
111impl Deref for Epoch {
112 type Target = u64;
113
114 fn deref(&self) -> &Self::Target {
115 &self.0
116 }
117}
118
119impl DerefMut for Epoch {
120 fn deref_mut(&mut self) -> &mut Self::Target {
121 &mut self.0
122 }
123}
124
125impl_add_to_wrapper!(Epoch, u64);
126impl_sub_to_wrapper!(Epoch, u64);
127impl_partial_eq_to_wrapper!(Epoch, u64);
128
129impl Display for Epoch {
130 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
131 write!(f, "{}", self.0)
132 }
133}
134
135impl TryInto<i64> for Epoch {
136 type Error = TryFromIntError;
137
138 fn try_into(self) -> Result<i64, Self::Error> {
139 self.0.try_into()
140 }
141}
142
143impl TryInto<i64> for &Epoch {
144 type Error = TryFromIntError;
145
146 fn try_into(self) -> Result<i64, Self::Error> {
147 self.0.try_into()
148 }
149}
150
151impl From<Epoch> for f64 {
152 fn from(value: Epoch) -> f64 {
153 value.0 as f64
154 }
155}
156
157impl FromStr for Epoch {
158 type Err = anyhow::Error;
159
160 fn from_str(epoch_str: &str) -> Result<Self, Self::Err> {
161 epoch_str.parse::<u64>().map(Epoch).with_context(|| {
162 format!("Invalid epoch '{epoch_str}': must be a positive 64-bit integer")
163 })
164 }
165}
166
167#[derive(Error, Debug)]
169pub enum EpochError {
170 #[error("epoch offset error")]
172 EpochOffset(u64, i64),
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum EpochSpecifier {
178 Number(Epoch),
180 Latest,
182 LatestMinusOffset(u64),
184}
185
186impl EpochSpecifier {
187 pub fn parse(epoch_str: &str) -> StdResult<Self> {
194 Self::from_str(epoch_str)
195 }
196}
197
198impl FromStr for EpochSpecifier {
199 type Err = StdError;
200
201 fn from_str(epoch_str: &str) -> Result<Self, Self::Err> {
202 if epoch_str == "latest" {
203 Ok(EpochSpecifier::Latest)
204 } else if let Some(offset_str) = epoch_str.strip_prefix("latest-") {
205 if offset_str.is_empty() {
206 anyhow::bail!("Invalid epoch '{epoch_str}': offset cannot be empty");
207 }
208 let offset = offset_str.parse::<u64>().with_context(|| {
209 format!("Invalid epoch '{epoch_str}': offset must be a positive 64-bit integer")
210 })?;
211
212 Ok(EpochSpecifier::LatestMinusOffset(offset))
213 } else {
214 epoch_str
215 .parse::<Epoch>()
216 .map(EpochSpecifier::Number)
217 .with_context(|| INVALID_EPOCH_SPECIFIER_ERROR)
218 }
219 }
220}
221
222impl Display for EpochSpecifier {
223 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
224 match self {
225 EpochSpecifier::Number(epoch) => write!(f, "{}", epoch),
226 EpochSpecifier::Latest => {
227 write!(f, "latest")
228 }
229 EpochSpecifier::LatestMinusOffset(offset) => {
230 write!(f, "latest-{}", offset)
231 }
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use crate::entities::arithmetic_operation_wrapper::tests::test_op_assign;
239
240 use super::*;
241
242 #[test]
243 fn test_display() {
244 assert_eq!(format!("{}", Epoch(72)), "72");
245 assert_eq!(format!("{}", &Epoch(13224)), "13224");
246 }
247
248 #[test]
249 fn test_serialize() {
250 assert_eq!(serde_json::to_string(&Epoch(72)).unwrap(), "72");
251 }
252
253 #[test]
254 fn test_deserialize() {
255 let block_number: Epoch = serde_json::from_str("13224").unwrap();
256 assert_eq!(block_number, Epoch(13224));
257 }
258
259 #[test]
260 #[allow(clippy::op_ref)]
261 fn test_add() {
262 assert_eq!(Epoch(4), Epoch(1) + Epoch(3));
263 assert_eq!(Epoch(4), Epoch(1) + 3_u64);
264 assert_eq!(Epoch(4), Epoch(1) + &3_u64);
265
266 assert_eq!(Epoch(4), 3_u64 + Epoch(1));
267 assert_eq!(Epoch(4), 3_u64 + &Epoch(1));
268 assert_eq!(Epoch(4), &3_u64 + Epoch(1));
269 assert_eq!(Epoch(4), &3_u64 + &Epoch(1));
270
271 test_op_assign!(Epoch(1), +=, Epoch(3) => Epoch(4));
272 test_op_assign!(Epoch(1), +=, 3_u64 => Epoch(4));
273 test_op_assign!(Epoch(1), +=, &3_u64 => Epoch(4));
274
275 test_op_assign!(1_u64, +=, Epoch(3) => 4_u64);
276 test_op_assign!(1_u64, +=, &Epoch(3) => 4_u64);
277 }
278
279 #[test]
280 #[allow(clippy::op_ref)]
281 fn test_sub() {
282 assert_eq!(Epoch(8), Epoch(14) - Epoch(6));
283 assert_eq!(Epoch(8), Epoch(14) - 6_u64);
284 assert_eq!(Epoch(8), Epoch(14) - &6_u64);
285
286 assert_eq!(Epoch(8), 6_u64 - Epoch(14));
287 assert_eq!(Epoch(8), 6_u64 - &Epoch(14));
288 assert_eq!(Epoch(8), &6_u64 - Epoch(14));
289 assert_eq!(Epoch(8), &6_u64 - &Epoch(14));
290
291 test_op_assign!(Epoch(14), -=, Epoch(6) => Epoch(8));
292 test_op_assign!(Epoch(14), -=, 6_u64 => Epoch(8));
293 test_op_assign!(Epoch(14), -=, &6_u64 => Epoch(8));
294
295 test_op_assign!(14_u64, -=, Epoch(6) => 8_u64);
296 test_op_assign!(14_u64, -=, &Epoch(6) => 8_u64);
297 }
298
299 #[test]
300 fn saturating_sub() {
301 assert_eq!(Epoch(0), Epoch(1) - Epoch(5));
302 assert_eq!(Epoch(0), Epoch(1) - 5_u64);
303 }
304
305 #[test]
306 fn test_previous() {
307 assert_eq!(Epoch(2), Epoch(3).previous().unwrap());
308 assert!(Epoch(0).previous().is_err());
309 }
310
311 #[test]
312 fn test_next() {
313 assert_eq!(Epoch(4), Epoch(3).next());
314 }
315
316 #[test]
317 fn test_eq() {
318 assert_eq!(Epoch(1), Epoch(1));
319 assert_eq!(Epoch(2), &Epoch(2));
320 assert_eq!(&Epoch(3), Epoch(3));
321 assert_eq!(&Epoch(4), &Epoch(4));
322
323 assert_eq!(Epoch(5), 5);
324 assert_eq!(Epoch(6), &6);
325 assert_eq!(&Epoch(7), 7);
326 assert_eq!(&Epoch(8), &8);
327
328 assert_eq!(9, Epoch(9));
329 assert_eq!(10, &Epoch(10));
330 assert_eq!(&11, Epoch(11));
331 assert_eq!(&12, &Epoch(12));
332 }
333
334 #[test]
335 fn test_has_gap_ok() {
336 assert!(Epoch(3).has_gap_with(&Epoch(5)));
337 assert!(!Epoch(3).has_gap_with(&Epoch(4)));
338 assert!(!Epoch(3).has_gap_with(&Epoch(3)));
339 assert!(!Epoch(3).has_gap_with(&Epoch(2)));
340 assert!(Epoch(3).has_gap_with(&Epoch(0)));
341 }
342
343 #[test]
344 fn from_str() {
345 let expected_epoch = Epoch(123);
346 let from_str = Epoch::from_str("123").unwrap();
347 assert_eq!(from_str, expected_epoch);
348
349 let from_string = String::from("123").parse::<Epoch>().unwrap();
350 assert_eq!(from_string, expected_epoch);
351
352 let alternate_notation: Epoch = "123".parse().unwrap();
353 assert_eq!(alternate_notation, expected_epoch);
354
355 let invalid_epoch_err = Epoch::from_str("123.456").unwrap_err();
356 assert!(
357 invalid_epoch_err
358 .to_string()
359 .contains("Invalid epoch '123.456': must be a positive 64-bit integer")
360 );
361
362 let overflow_err = format!("1{}", u64::MAX).parse::<Epoch>().unwrap_err();
363 assert!(
364 overflow_err.to_string().contains(
365 "Invalid epoch '118446744073709551615': must be a positive 64-bit integer"
366 )
367 );
368 }
369
370 #[test]
371 fn display_epoch_specifier() {
372 assert_eq!(format!("{}", EpochSpecifier::Number(Epoch(123))), "123");
373 assert_eq!(format!("{}", EpochSpecifier::Latest), "latest");
374 assert_eq!(
375 format!("{}", EpochSpecifier::LatestMinusOffset(123)),
376 "latest-123"
377 );
378 }
379
380 mod parse_specifier {
381 use super::*;
382
383 #[test]
384 fn parse_epoch_number() {
385 let parsed_value = EpochSpecifier::parse("5").unwrap();
386 assert_eq!(EpochSpecifier::Number(Epoch(5)), parsed_value);
387 }
388
389 #[test]
390 fn parse_latest_epoch() {
391 let parsed_value = EpochSpecifier::parse("latest").unwrap();
392 assert_eq!(EpochSpecifier::Latest, parsed_value);
393 }
394
395 #[test]
396 fn parse_latest_epoch_with_offset() {
397 let parsed_value = EpochSpecifier::parse("latest-43").unwrap();
398 assert_eq!(EpochSpecifier::LatestMinusOffset(43), parsed_value);
399 }
400
401 #[test]
402 fn parse_invalid_str_yield_error() {
403 let error = EpochSpecifier::parse("invalid_string").unwrap_err();
404 assert!(error.to_string().contains(INVALID_EPOCH_SPECIFIER_ERROR));
405 }
406
407 #[test]
408 fn parse_too_big_epoch_number_yield_error() {
409 let error = EpochSpecifier::parse(&format!("9{}", u64::MAX)).unwrap_err();
410 assert!(error.to_string().contains(INVALID_EPOCH_SPECIFIER_ERROR));
411 println!("{:?}", error);
412 }
413
414 #[test]
415 fn parse_latest_epoch_with_invalid_offset_yield_error() {
416 let error = EpochSpecifier::parse("latest-invalid").unwrap_err();
417 assert!(error.to_string().contains(
418 "Invalid epoch 'latest-invalid': offset must be a positive 64-bit integer"
419 ));
420 }
421
422 #[test]
423 fn parse_latest_epoch_with_empty_offset_yield_error() {
424 let error = EpochSpecifier::parse("latest-").unwrap_err();
425 assert!(
426 error
427 .to_string()
428 .contains("Invalid epoch 'latest-': offset cannot be empty")
429 );
430 }
431
432 #[test]
433 fn parse_latest_epoch_with_too_big_offset_yield_error() {
434 let error = EpochSpecifier::parse(&format!("latest-9{}", u64::MAX)).unwrap_err();
435 assert!(error.to_string().contains(
436 "Invalid epoch 'latest-918446744073709551615': offset must be a positive 64-bit integer"
437 ))
438 }
439
440 #[test]
441 fn specifier_to_string_can_be_parsed_back() {
442 for specifier in [
443 EpochSpecifier::Number(Epoch(121)),
444 EpochSpecifier::Latest,
445 EpochSpecifier::LatestMinusOffset(121),
446 ] {
447 let value = EpochSpecifier::parse(&specifier.to_string()).unwrap();
448 assert_eq!(value, specifier);
449 }
450 }
451 }
452}