Skip to main content

tronz_primitives/
amount.rs

1//! TRX amount type.
2//!
3//! TRON denominates value in *sun*, where `1 TRX = 1_000_000 sun`. [`Trx`]
4//! wraps an `i64` sun value to match the protobuf `sint64` representation.
5
6use core::fmt;
7use core::ops::{Add, Sub};
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::AmountError;
12
13/// Number of sun in one TRX.
14pub const SUN_PER_TRX: i64 = 1_000_000;
15
16/// An amount of TRX, stored internally as `i64` sun.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct Trx(i64);
20
21impl Trx {
22    /// Zero TRX.
23    pub const ZERO: Trx = Trx(0);
24
25    /// Construct directly from a sun value without validation.
26    ///
27    /// Negative values are representable here because malformed on-chain data
28    /// must round-trip without panicking; prefer [`Trx::from_sun`] for
29    /// user-facing input.
30    pub const fn from_sun_unchecked(sun: i64) -> Self {
31        Self(sun)
32    }
33
34    /// Construct from a sun value, rejecting negatives.
35    pub fn from_sun(sun: i64) -> Result<Self, AmountError> {
36        if sun < 0 {
37            return Err(AmountError::Negative(sun));
38        }
39        Ok(Self(sun))
40    }
41
42    /// Construct from a floating-point TRX value (e.g. `1.5` TRX).
43    ///
44    /// Rejects negative and non-finite values, and values that overflow the
45    /// `i64` sun range.
46    pub fn from_trx(trx: f64) -> Result<Self, AmountError> {
47        if !trx.is_finite() || trx < 0.0 {
48            return Err(AmountError::OutOfRange(trx));
49        }
50        let sun = trx * SUN_PER_TRX as f64;
51        if sun > i64::MAX as f64 {
52            return Err(AmountError::OutOfRange(trx));
53        }
54        Ok(Self(sun.round() as i64))
55    }
56
57    /// The raw sun value.
58    pub const fn as_sun(self) -> i64 {
59        self.0
60    }
61
62    /// The value expressed as floating-point TRX (lossy for large amounts).
63    pub fn as_trx(self) -> f64 {
64        self.0 as f64 / SUN_PER_TRX as f64
65    }
66
67    /// Checked addition, returning `None` on `i64` overflow.
68    pub fn checked_add(self, rhs: Trx) -> Option<Trx> {
69        self.0.checked_add(rhs.0).map(Trx)
70    }
71
72    /// Checked subtraction, returning `None` on `i64` overflow.
73    pub fn checked_sub(self, rhs: Trx) -> Option<Trx> {
74        self.0.checked_sub(rhs.0).map(Trx)
75    }
76}
77
78impl Add for Trx {
79    type Output = Trx;
80    fn add(self, rhs: Trx) -> Trx {
81        Trx(self.0 + rhs.0)
82    }
83}
84
85impl Sub for Trx {
86    type Output = Trx;
87    fn sub(self, rhs: Trx) -> Trx {
88        Trx(self.0 - rhs.0)
89    }
90}
91
92impl fmt::Display for Trx {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{} TRX", self.as_trx())
95    }
96}
97
98impl fmt::Debug for Trx {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "Trx({} sun)", self.0)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn conversions() {
110        assert_eq!(Trx::from_trx(1.0).unwrap().as_sun(), 1_000_000);
111        assert_eq!(Trx::from_trx(1.5).unwrap().as_sun(), 1_500_000);
112        assert_eq!(Trx::from_sun(2_500_000).unwrap().as_trx(), 2.5);
113    }
114
115    #[test]
116    fn rejects_negative() {
117        assert!(Trx::from_sun(-1).is_err());
118        assert!(Trx::from_trx(-1.0).is_err());
119        assert!(Trx::from_trx(f64::NAN).is_err());
120    }
121
122    #[test]
123    fn unchecked_allows_negative() {
124        assert_eq!(Trx::from_sun_unchecked(-5).as_sun(), -5);
125    }
126
127    #[test]
128    fn arithmetic() {
129        let a = Trx::from_trx(1.0).unwrap();
130        let b = Trx::from_trx(0.5).unwrap();
131        assert_eq!((a + b).as_sun(), 1_500_000);
132        assert_eq!((a - b).as_sun(), 500_000);
133        assert_eq!(a.checked_add(b), Some(Trx::from_sun(1_500_000).unwrap()));
134    }
135}