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)]
28#[serde(transparent)]
29pub struct Trx(i64);
30
31impl Trx {
32 pub const ZERO: Trx = Trx(0);
34
35 #[doc(hidden)]
42 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 {
98 type Err = AmountError;
99
100 fn from_str(s: &str) -> Result<Self, Self::Err> {
101 if s.starts_with('-') || !s.is_ascii() {
102 return Err(AmountError::ParseError(s.to_owned()));
103 }
104
105 let mut normalized = s.to_owned();
106 let decimal_len = if let Some(decimal_index) = normalized.find('.') {
107 normalized.remove(decimal_index);
108 normalized[decimal_index..].len()
109 } else {
110 0
111 };
112
113 if decimal_len > 6 {
116 normalized.truncate(normalized.len() - (decimal_len - 6));
117 }
118
119 let mut value = 0u64;
120 for byte in normalized.bytes() {
121 if byte == b'_' {
122 continue;
123 }
124 let digit = match byte {
125 b'0'..=b'9' => (byte - b'0') as u64,
126 _ => return Err(AmountError::ParseError(s.to_owned())),
127 };
128 value = value
129 .checked_mul(10)
130 .and_then(|v| v.checked_add(digit))
131 .ok_or_else(|| AmountError::ParseError(s.to_owned()))?;
132 }
133
134 let scale = 6usize.saturating_sub(decimal_len);
135 let value = value
136 .checked_mul(10u64.pow(scale as u32))
137 .filter(|&v| v <= MAX_SUN)
138 .ok_or_else(|| AmountError::ParseError(s.to_owned()))?;
139 Ok(Self(value as i64))
140 }
141}
142
143pub fn parse_trx(s: &str) -> Result<Trx, AmountError> {
155 s.parse()
156}
157
158pub fn format_trx(amount: Trx) -> String {
170 amount.to_string()
171}
172
173impl Add for Trx {
174 type Output = Trx;
175 fn add(self, rhs: Trx) -> Trx {
180 self.checked_add(rhs).expect("TRX addition overflows or contains a negative operand")
181 }
182}
183
184impl Sub for Trx {
185 type Output = Trx;
186 fn sub(self, rhs: Trx) -> Trx {
191 self.checked_sub(rhs)
192 .expect("TRX subtraction underflows, overflows, or contains a negative operand")
193 }
194}
195
196impl fmt::Display for Trx {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 let abs = self.0.unsigned_abs();
210 let whole = abs / SUN_PER_TRX as u64;
211 let frac = abs % SUN_PER_TRX as u64;
212 let sign = if self.0 < 0 { "-" } else { "" };
213 write!(f, "{sign}{whole}.{frac:06}")
214 }
215}
216
217impl fmt::Debug for Trx {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 write!(f, "Trx({} sun)", self.0)
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn sun(value: i64) -> Trx {
228 Trx::from_sun(value).unwrap()
229 }
230
231 #[test]
232 fn conversions() {
233 assert_eq!("1".parse::<Trx>().unwrap().as_sun(), 1_000_000);
234 assert_eq!("1.5".parse::<Trx>().unwrap().as_sun(), 1_500_000);
235 }
236
237 #[test]
238 fn rejects_negative() {
239 assert!(Trx::from_sun(-1).is_err());
240 assert!("-1".parse::<Trx>().is_err());
241 }
242
243 #[test]
244 fn unchecked_allows_negative() {
245 assert_eq!(Trx::from_sun_unchecked(-5).as_sun(), -5);
246 }
247
248 #[test]
249 fn arithmetic() {
250 let a = "1".parse::<Trx>().unwrap();
251 let b = "0.5".parse::<Trx>().unwrap();
252 assert_eq!((a + b).as_sun(), 1_500_000);
253 assert_eq!((a - b).as_sun(), 500_000);
254 assert_eq!(a.checked_add(b), Some(Trx::from_sun(1_500_000).unwrap()));
255 }
256
257 #[test]
258 fn parse_valid() {
259 assert_eq!("1".parse::<Trx>().unwrap().as_sun(), 1_000_000);
260 assert_eq!("1.5".parse::<Trx>().unwrap().as_sun(), 1_500_000);
261 assert_eq!(".5".parse::<Trx>().unwrap().as_sun(), 500_000);
262 assert_eq!("0.000001".parse::<Trx>().unwrap().as_sun(), 1);
263 assert_eq!("100".parse::<Trx>().unwrap().as_sun(), 100_000_000);
264 assert_eq!("1.000000".parse::<Trx>().unwrap().as_sun(), 1_000_000);
265 assert_eq!("1_000".parse::<Trx>().unwrap().as_sun(), 1_000_000_000);
266 assert_eq!("1.".parse::<Trx>().unwrap().as_sun(), 1_000_000);
267 assert_eq!("".parse::<Trx>().unwrap(), Trx::ZERO);
268 }
269
270 #[test]
271 fn parse_invalid() {
272 assert!("-1".parse::<Trx>().is_err());
273 assert!("abc".parse::<Trx>().is_err());
274 assert!("1.abc".parse::<Trx>().is_err());
275 assert!(" 1 ".parse::<Trx>().is_err());
276 assert!("+1".parse::<Trx>().is_err());
277 assert!("1.金额".parse::<Trx>().is_err());
278 }
279
280 #[test]
281 fn parse_truncates_beyond_sun_precision() {
282 assert_eq!("1.1234567".parse::<Trx>().unwrap().as_sun(), 1_123_456);
283 assert_eq!("0.0000009".parse::<Trx>().unwrap(), Trx::ZERO);
284 }
285
286 #[test]
287 fn display_is_exact() {
288 assert_eq!(sun(1_500_000).to_string(), "1.500000");
289 assert_eq!("100".parse::<Trx>().unwrap().to_string(), "100.000000");
290 assert_eq!(sun(1).to_string(), "0.000001");
291 assert_eq!(Trx::ZERO.to_string(), "0.000000");
292 assert_eq!(Trx::from_sun_unchecked(-1_500_000).to_string(), "-1.500000");
293 }
294
295 #[test]
296 fn display_parse_round_trip() {
297 for &sun in &[0, 1, 1_000_000, 1_500_000, 100_000_000, 123_456] {
298 let t = Trx::from_sun(sun).unwrap();
299 assert_eq!(t.to_string().parse::<Trx>().unwrap(), t);
300 }
301 }
302
303 #[test]
304 fn alloy_style_helpers() {
305 assert_eq!(parse_trx("1.5").unwrap().as_sun(), 1_500_000);
306 assert_eq!(format_trx(sun(1_500_000)), "1.500000");
307 }
308
309 #[test]
310 fn matches_alloy_unit_helpers_within_tron_range() {
311 for input in ["", ".5", "1.", "1_000", "1.1234567", "9223372036854.775807"] {
312 let alloy = alloy_primitives::utils::parse_units(input, 6).unwrap();
313 let expected = u64::try_from(alloy).unwrap();
314 assert_eq!(input.parse::<Trx>().unwrap().as_sun(), expected as i64);
315 }
316
317 for amount in [Trx::ZERO, sun(1), sun(1_500_000), "100".parse().unwrap()] {
318 let alloy = alloy_primitives::utils::format_units(amount.as_sun(), 6).unwrap();
319 assert_eq!(amount.to_string(), alloy);
320 }
321 }
322
323 #[test]
324 fn parse_accepts_max_i64_sun() {
325 let max = "9223372036854.775807".parse::<Trx>().unwrap();
326 assert_eq!(max.as_sun(), i64::MAX);
327 }
328
329 #[test]
330 fn parse_rejects_above_max_i64_sun() {
331 assert!("9223372036854.775808".parse::<Trx>().is_err());
332 }
333
334 #[test]
335 fn checked_sub_rejects_negative() {
336 assert!(Trx::ZERO.checked_sub(sun(1)).is_none());
337 }
338
339 #[test]
340 fn checked_arithmetic_rejects_negative_operands() {
341 let negative = Trx::from_sun_unchecked(-5);
342 assert!(sun(10).checked_add(negative).is_none());
343 assert!(negative.checked_add(sun(10)).is_none());
344 assert!(sun(10).checked_sub(negative).is_none());
345 assert!(negative.checked_sub(sun(10)).is_none());
346 }
347}