tronz_primitives/
amount.rs1use core::{
7 fmt,
8 ops::{Add, Sub},
9 str::FromStr,
10};
11
12use serde::{Deserialize, Serialize};
13
14use crate::error::AmountError;
15
16const MAX_SUN: u64 = i64::MAX as u64;
18
19pub const SUN_PER_TRX: i64 = 1_000_000;
21
22#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct Trx(i64);
33
34impl Trx {
35 pub const ZERO: Trx = Trx(0);
37
38 pub const fn from_sun_unchecked(sun: i64) -> Self {
43 Self(sun)
44 }
45
46 pub const fn from_sun(sun: i64) -> Result<Self, AmountError> {
48 if sun < 0 {
49 return Err(AmountError::Negative(sun));
50 }
51 Ok(Self(sun))
52 }
53
54 pub const fn as_sun(self) -> i64 {
56 self.0
57 }
58
59 pub fn checked_add(self, rhs: Trx) -> Option<Trx> {
62 if self.0 < 0 || rhs.0 < 0 {
63 return None;
64 }
65 self.0.checked_add(rhs.0).filter(|&v| v >= 0).map(Trx)
66 }
67
68 pub fn checked_sub(self, rhs: Trx) -> Option<Trx> {
71 if self.0 < 0 || rhs.0 < 0 {
72 return None;
73 }
74 self.0.checked_sub(rhs.0).filter(|&v| v >= 0).map(Trx)
75 }
76}
77
78impl FromStr for Trx {
79 type Err = AmountError;
80
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 if s.starts_with('-') || !s.is_ascii() {
83 return Err(AmountError::ParseError(s.to_owned()));
84 }
85
86 let mut normalized = s.to_owned();
87 let decimal_len = if let Some(decimal_index) = normalized.find('.') {
88 normalized.remove(decimal_index);
89 normalized[decimal_index..].len()
90 } else {
91 0
92 };
93
94 if decimal_len > 6 {
97 normalized.truncate(normalized.len() - (decimal_len - 6));
98 }
99
100 let mut value = 0u64;
101 for byte in normalized.bytes() {
102 if byte == b'_' {
103 continue;
104 }
105 let digit = match byte {
106 b'0'..=b'9' => (byte - b'0') as u64,
107 _ => return Err(AmountError::ParseError(s.to_owned())),
108 };
109 value = value
110 .checked_mul(10)
111 .and_then(|v| v.checked_add(digit))
112 .ok_or_else(|| AmountError::ParseError(s.to_owned()))?;
113 }
114
115 let scale = 6usize.saturating_sub(decimal_len);
116 let value = value
117 .checked_mul(10u64.pow(scale as u32))
118 .filter(|&v| v <= MAX_SUN)
119 .ok_or_else(|| AmountError::ParseError(s.to_owned()))?;
120 Ok(Self(value as i64))
121 }
122}
123
124pub fn parse_trx(s: &str) -> Result<Trx, AmountError> {
146 s.parse()
147}
148
149pub fn format_trx(amount: Trx) -> String {
163 amount.to_string()
164}
165
166impl Add for Trx {
167 type Output = Trx;
168 fn add(self, rhs: Trx) -> Trx {
173 self.checked_add(rhs).expect("TRX addition overflows or contains a negative operand")
174 }
175}
176
177impl Sub for Trx {
178 type Output = Trx;
179 fn sub(self, rhs: Trx) -> Trx {
184 self.checked_sub(rhs)
185 .expect("TRX subtraction underflows, overflows, or contains a negative operand")
186 }
187}
188
189impl fmt::Display for Trx {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 let abs = self.0.unsigned_abs();
192 let whole = abs / SUN_PER_TRX as u64;
193 let frac = abs % SUN_PER_TRX as u64;
194 let sign = if self.0 < 0 { "-" } else { "" };
195 write!(f, "{sign}{whole}.{frac:06}")
196 }
197}
198
199impl fmt::Debug for Trx {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 write!(f, "Trx({} sun)", self.0)
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn sun(value: i64) -> Trx {
210 Trx::from_sun(value).unwrap()
211 }
212
213 #[test]
214 fn conversions() {
215 assert_eq!("1".parse::<Trx>().unwrap().as_sun(), 1_000_000);
216 assert_eq!("1.5".parse::<Trx>().unwrap().as_sun(), 1_500_000);
217 }
218
219 #[test]
220 fn rejects_negative() {
221 assert!(Trx::from_sun(-1).is_err());
222 assert!("-1".parse::<Trx>().is_err());
223 }
224
225 #[test]
226 fn unchecked_allows_negative() {
227 assert_eq!(Trx::from_sun_unchecked(-5).as_sun(), -5);
228 }
229
230 #[test]
231 fn arithmetic() {
232 let a = "1".parse::<Trx>().unwrap();
233 let b = "0.5".parse::<Trx>().unwrap();
234 assert_eq!((a + b).as_sun(), 1_500_000);
235 assert_eq!((a - b).as_sun(), 500_000);
236 assert_eq!(a.checked_add(b), Some(Trx::from_sun(1_500_000).unwrap()));
237 }
238
239 #[test]
240 fn parse_valid() {
241 assert_eq!("1".parse::<Trx>().unwrap().as_sun(), 1_000_000);
242 assert_eq!("1.5".parse::<Trx>().unwrap().as_sun(), 1_500_000);
243 assert_eq!(".5".parse::<Trx>().unwrap().as_sun(), 500_000);
244 assert_eq!("0.000001".parse::<Trx>().unwrap().as_sun(), 1);
245 assert_eq!("100".parse::<Trx>().unwrap().as_sun(), 100_000_000);
246 assert_eq!("1.000000".parse::<Trx>().unwrap().as_sun(), 1_000_000);
247 assert_eq!("1_000".parse::<Trx>().unwrap().as_sun(), 1_000_000_000);
248 assert_eq!("1.".parse::<Trx>().unwrap().as_sun(), 1_000_000);
249 assert_eq!("".parse::<Trx>().unwrap(), Trx::ZERO);
250 }
251
252 #[test]
253 fn parse_invalid() {
254 assert!("-1".parse::<Trx>().is_err());
255 assert!("abc".parse::<Trx>().is_err());
256 assert!("1.abc".parse::<Trx>().is_err());
257 assert!(" 1 ".parse::<Trx>().is_err());
258 assert!("+1".parse::<Trx>().is_err());
259 assert!("1.金额".parse::<Trx>().is_err());
260 }
261
262 #[test]
263 fn parse_truncates_beyond_sun_precision() {
264 assert_eq!("1.1234567".parse::<Trx>().unwrap().as_sun(), 1_123_456);
265 assert_eq!("0.0000009".parse::<Trx>().unwrap(), Trx::ZERO);
266 }
267
268 #[test]
269 fn display_is_exact() {
270 assert_eq!(sun(1_500_000).to_string(), "1.500000");
271 assert_eq!("100".parse::<Trx>().unwrap().to_string(), "100.000000");
272 assert_eq!(sun(1).to_string(), "0.000001");
273 assert_eq!(Trx::ZERO.to_string(), "0.000000");
274 assert_eq!(Trx::from_sun_unchecked(-1_500_000).to_string(), "-1.500000");
275 }
276
277 #[test]
278 fn display_parse_round_trip() {
279 for &sun in &[0, 1, 1_000_000, 1_500_000, 100_000_000, 123_456] {
280 let t = Trx::from_sun(sun).unwrap();
281 assert_eq!(t.to_string().parse::<Trx>().unwrap(), t);
282 }
283 }
284
285 #[test]
286 fn alloy_style_helpers() {
287 assert_eq!(parse_trx("1.5").unwrap().as_sun(), 1_500_000);
288 assert_eq!(format_trx(sun(1_500_000)), "1.500000");
289 }
290
291 #[test]
292 fn matches_alloy_unit_helpers_within_tron_range() {
293 for input in ["", ".5", "1.", "1_000", "1.1234567", "9223372036854.775807"] {
294 let alloy = alloy_primitives::utils::parse_units(input, 6).unwrap();
295 let expected = u64::try_from(alloy).unwrap();
296 assert_eq!(input.parse::<Trx>().unwrap().as_sun(), expected as i64);
297 }
298
299 for amount in [Trx::ZERO, sun(1), sun(1_500_000), "100".parse().unwrap()] {
300 let alloy = alloy_primitives::utils::format_units(amount.as_sun(), 6).unwrap();
301 assert_eq!(amount.to_string(), alloy);
302 }
303 }
304
305 #[test]
306 fn parse_accepts_max_i64_sun() {
307 let max = "9223372036854.775807".parse::<Trx>().unwrap();
308 assert_eq!(max.as_sun(), i64::MAX);
309 }
310
311 #[test]
312 fn parse_rejects_above_max_i64_sun() {
313 assert!("9223372036854.775808".parse::<Trx>().is_err());
314 }
315
316 #[test]
317 fn checked_sub_rejects_negative() {
318 assert!(Trx::ZERO.checked_sub(sun(1)).is_none());
319 }
320
321 #[test]
322 fn checked_arithmetic_rejects_negative_operands() {
323 let negative = Trx::from_sun_unchecked(-5);
324 assert!(sun(10).checked_add(negative).is_none());
325 assert!(negative.checked_add(sun(10)).is_none());
326 assert!(sun(10).checked_sub(negative).is_none());
327 assert!(negative.checked_sub(sun(10)).is_none());
328 }
329}