canic_types/
cycles.rs

1//!
2//! Cycle-denominated helpers covering human-friendly parsing, config shorthands,
3//! and arithmetic wrappers used by ops modules.
4//! The constants offer readable units (K/M/B/T/Q) while `Cycles` wraps `Nat`
5//! with derived math traits and serde helpers.
6//!
7
8use canic_cdk::{candid::CandidType, types::Nat};
9use derive_more::{Add, AddAssign, Sub, SubAssign};
10use num_traits::cast::ToPrimitive;
11use serde::{Deserialize, Serialize, de::Deserializer};
12use std::{
13    fmt::{self, Display},
14    str::FromStr,
15};
16
17///
18/// Constants
19/// Cycle unit shorthands for configs and logs
20///
21
22pub const KC: u128 = 1_000;
23pub const MC: u128 = 1_000_000;
24pub const BC: u128 = 1_000_000_000;
25pub const TC: u128 = 1_000_000_000_000;
26pub const QC: u128 = 1_000_000_000_000_000;
27
28///
29/// Cycles
30/// Thin wrapper around `Nat` that carries helper traits and serializers for
31/// arithmetic on cycle balances.
32///
33
34#[derive(
35    Add,
36    AddAssign,
37    CandidType,
38    Clone,
39    Default,
40    Debug,
41    Deserialize,
42    PartialEq,
43    Eq,
44    Hash,
45    PartialOrd,
46    Ord,
47    Serialize,
48    SubAssign,
49    Sub,
50)]
51pub struct Cycles(Nat);
52
53impl Cycles {
54    #[must_use]
55    pub fn new(amount: u128) -> Self {
56        Self(amount.into())
57    }
58
59    #[must_use]
60    pub fn to_u64(&self) -> u64 {
61        self.0.0.to_u64().unwrap_or(u64::MAX)
62    }
63
64    #[must_use]
65    pub fn to_u128(&self) -> u128 {
66        self.0.0.to_u128().unwrap_or(u128::MAX)
67    }
68
69    // from_config
70    // accepts the short hand 10T format or a number
71    pub fn from_config<'de, D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        #[derive(Deserialize)]
76        #[serde(untagged)]
77        enum Helper {
78            Str(String),
79            Num(u128),
80        }
81
82        match Helper::deserialize(deserializer)? {
83            Helper::Str(s) => s.parse::<Self>().map_err(serde::de::Error::custom),
84            Helper::Num(n) => Ok(Self::new(n)),
85        }
86    }
87}
88
89#[allow(clippy::cast_precision_loss)]
90impl Display for Cycles {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        // default format in TeraCycles
93        write!(f, "{:.3} TC", self.to_u128() as f64 / 1_000_000_000_000f64)
94    }
95}
96
97impl From<u128> for Cycles {
98    fn from(v: u128) -> Self {
99        Self(Nat::from(v))
100    }
101}
102
103impl From<Nat> for Cycles {
104    fn from(n: Nat) -> Self {
105        Self(n)
106    }
107}
108
109impl From<Cycles> for Nat {
110    fn from(c: Cycles) -> Self {
111        c.0
112    }
113}
114
115// Human-input parser: "10K", "1.5T", etc.
116#[allow(clippy::cast_precision_loss)]
117#[allow(clippy::cast_sign_loss)]
118#[allow(clippy::cast_possible_truncation)]
119impl FromStr for Cycles {
120    type Err = String;
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        let mut num = String::new();
123        let mut suf = String::new();
124        let mut suf_count = 0;
125        for c in s.chars() {
126            if c.is_ascii_digit() || c == '.' {
127                if suf_count > 0 {
128                    return Err("invalid suffix".to_string());
129                }
130                num.push(c);
131            } else if suf_count >= 2 {
132                return Err("invalid suffix".to_string());
133            } else {
134                suf.push(c);
135                suf_count += 1;
136            }
137        }
138
139        let mut n = num.parse::<f64>().map_err(|e| e.to_string())?;
140        match suf.as_str() {
141            "" => {}
142            "K" => n *= KC as f64,
143            "M" => n *= MC as f64,
144            "B" => n *= BC as f64,
145            "T" => n *= TC as f64,
146            "Q" => n *= QC as f64,
147            _ => return Err("invalid suffix".to_string()),
148        }
149
150        Ok(Self::new(n as u128))
151    }
152}