Skip to main content

canic_cdk/types/
cycles.rs

1use crate::{
2    candid::{CandidType, Nat},
3    structures::{Storable, storable::Bound},
4};
5use serde::{Deserialize, Serialize, de::Deserializer};
6use std::{
7    borrow::Cow,
8    fmt::{self, Display},
9    ops::{Add, AddAssign, Sub, SubAssign},
10    str::FromStr,
11};
12
13///
14/// Constants
15/// Cycle unit shorthands for configs and logs
16///
17
18pub const KC: u128 = 1_000;
19pub const MC: u128 = 1_000_000;
20pub const BC: u128 = 1_000_000_000;
21pub const TC: u128 = 1_000_000_000_000;
22pub const QC: u128 = 1_000_000_000_000_000;
23
24///
25/// Cycles
26/// Thin wrapper around `Nat` that carries helper traits and serializers for
27/// arithmetic on cycle balances.
28///
29
30#[derive(
31    CandidType, Clone, Default, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize,
32)]
33pub struct Cycles(u128);
34
35impl Cycles {
36    #[must_use]
37    pub const fn new(n: u128) -> Self {
38        Self(n)
39    }
40
41    #[must_use]
42    pub fn to_u64(&self) -> u64 {
43        u64::try_from(self.0).unwrap_or(u64::MAX)
44    }
45
46    #[must_use]
47    pub const fn to_u128(&self) -> u128 {
48        self.0
49    }
50
51    // from_config
52    // accepts the short hand 10T format or a number
53    pub fn from_config<'de, D>(deserializer: D) -> Result<Self, D::Error>
54    where
55        D: Deserializer<'de>,
56    {
57        #[derive(Deserialize)]
58        #[serde(untagged)]
59        enum Helper {
60            Str(String),
61            Num(u128),
62        }
63
64        match Helper::deserialize(deserializer)? {
65            Helper::Str(s) => s.parse::<Self>().map_err(serde::de::Error::custom),
66            Helper::Num(n) => Ok(Self::new(n)),
67        }
68    }
69}
70
71#[expect(clippy::cast_precision_loss)]
72impl Display for Cycles {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        // default format in TeraCycles
75        write!(f, "{:.3} TC", self.to_u128() as f64 / 1_000_000_000_000f64)
76    }
77}
78
79impl From<Nat> for Cycles {
80    fn from(n: Nat) -> Self {
81        Self(u128::try_from(n.0).unwrap_or(0))
82    }
83}
84
85impl From<u128> for Cycles {
86    fn from(v: u128) -> Self {
87        Self(v)
88    }
89}
90
91impl From<Cycles> for u128 {
92    fn from(c: Cycles) -> Self {
93        c.0
94    }
95}
96
97impl Add for Cycles {
98    type Output = Self;
99
100    // Add two cycle balances while preserving the wrapper type.
101    fn add(self, rhs: Self) -> Self::Output {
102        Self(self.0 + rhs.0)
103    }
104}
105
106impl AddAssign for Cycles {
107    // Accumulate another cycle balance into this wrapper.
108    fn add_assign(&mut self, rhs: Self) {
109        self.0 += rhs.0;
110    }
111}
112
113impl Sub for Cycles {
114    type Output = Self;
115
116    // Subtract one cycle balance from another inside the wrapper type.
117    fn sub(self, rhs: Self) -> Self::Output {
118        Self(self.0 - rhs.0)
119    }
120}
121
122impl SubAssign for Cycles {
123    // Reduce this wrapper by another cycle balance in place.
124    fn sub_assign(&mut self, rhs: Self) {
125        self.0 -= rhs.0;
126    }
127}
128
129// Human-input parser: "10K", "1.5T", etc.
130#[expect(clippy::cast_precision_loss)]
131#[expect(clippy::cast_sign_loss)]
132#[expect(clippy::cast_possible_truncation)]
133impl FromStr for Cycles {
134    type Err = String;
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        let mut num = String::new();
137        let mut suf = String::new();
138        let mut suf_count = 0;
139        for c in s.chars() {
140            if c.is_ascii_digit() || c == '.' {
141                if suf_count > 0 {
142                    return Err("invalid suffix".to_string());
143                }
144                num.push(c);
145            } else if suf_count >= 2 {
146                return Err("invalid suffix".to_string());
147            } else {
148                suf.push(c);
149                suf_count += 1;
150            }
151        }
152
153        let mut n = num.parse::<f64>().map_err(|e| e.to_string())?;
154        match suf.as_str() {
155            "" => {}
156            "K" => n *= KC as f64,
157            "M" => n *= MC as f64,
158            "B" => n *= BC as f64,
159            "T" => n *= TC as f64,
160            "Q" => n *= QC as f64,
161            _ => return Err("invalid suffix".to_string()),
162        }
163
164        Ok(Self::new(n as u128))
165    }
166}
167
168impl Storable for Cycles {
169    // u128 is exactly 16 bytes, fixed-size
170    const BOUND: Bound = Bound::Bounded {
171        max_size: 16,
172        is_fixed_size: true,
173    };
174
175    fn to_bytes(&self) -> Cow<'_, [u8]> {
176        Cow::Owned(self.0.to_be_bytes().to_vec())
177    }
178
179    fn into_bytes(self) -> Vec<u8> {
180        self.0.to_be_bytes().to_vec()
181    }
182
183    fn from_bytes(bytes: Cow<[u8]>) -> Self {
184        let b = bytes.as_ref();
185
186        // Defensive decode: never panic on corrupted data
187        if b.len() != 16 {
188            return Self::default();
189        }
190
191        let mut arr = [0u8; 16];
192        arr.copy_from_slice(b);
193
194        Self(u128::from_be_bytes(arr))
195    }
196}