use core::{
fmt,
ops::{Add, Sub},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use crate::error::AmountError;
const MAX_SUN: u64 = i64::MAX as u64;
pub const SUN_PER_TRX: i64 = 1_000_000;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Trx(i64);
impl Trx {
pub const ZERO: Trx = Trx(0);
#[doc(hidden)]
pub const fn from_sun_unchecked(sun: i64) -> Self {
Self(sun)
}
pub const fn from_sun(sun: i64) -> Result<Self, AmountError> {
if sun < 0 {
return Err(AmountError::Negative(sun));
}
Ok(Self(sun))
}
pub const fn as_sun(self) -> i64 {
self.0
}
pub fn checked_add(self, rhs: Trx) -> Option<Trx> {
if self.0 < 0 || rhs.0 < 0 {
return None;
}
self.0.checked_add(rhs.0).filter(|&v| v >= 0).map(Trx)
}
pub fn checked_sub(self, rhs: Trx) -> Option<Trx> {
if self.0 < 0 || rhs.0 < 0 {
return None;
}
self.0.checked_sub(rhs.0).filter(|&v| v >= 0).map(Trx)
}
}
impl FromStr for Trx {
type Err = AmountError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.starts_with('-') || !s.is_ascii() {
return Err(AmountError::ParseError(s.to_owned()));
}
let mut normalized = s.to_owned();
let decimal_len = if let Some(decimal_index) = normalized.find('.') {
normalized.remove(decimal_index);
normalized[decimal_index..].len()
} else {
0
};
if decimal_len > 6 {
normalized.truncate(normalized.len() - (decimal_len - 6));
}
let mut value = 0u64;
for byte in normalized.bytes() {
if byte == b'_' {
continue;
}
let digit = match byte {
b'0'..=b'9' => (byte - b'0') as u64,
_ => return Err(AmountError::ParseError(s.to_owned())),
};
value = value
.checked_mul(10)
.and_then(|v| v.checked_add(digit))
.ok_or_else(|| AmountError::ParseError(s.to_owned()))?;
}
let scale = 6usize.saturating_sub(decimal_len);
let value = value
.checked_mul(10u64.pow(scale as u32))
.filter(|&v| v <= MAX_SUN)
.ok_or_else(|| AmountError::ParseError(s.to_owned()))?;
Ok(Self(value as i64))
}
}
pub fn parse_trx(s: &str) -> Result<Trx, AmountError> {
s.parse()
}
pub fn format_trx(amount: Trx) -> String {
amount.to_string()
}
impl Add for Trx {
type Output = Trx;
fn add(self, rhs: Trx) -> Trx {
self.checked_add(rhs).expect("TRX addition overflows or contains a negative operand")
}
}
impl Sub for Trx {
type Output = Trx;
fn sub(self, rhs: Trx) -> Trx {
self.checked_sub(rhs)
.expect("TRX subtraction underflows, overflows, or contains a negative operand")
}
}
impl fmt::Display for Trx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let abs = self.0.unsigned_abs();
let whole = abs / SUN_PER_TRX as u64;
let frac = abs % SUN_PER_TRX as u64;
let sign = if self.0 < 0 { "-" } else { "" };
write!(f, "{sign}{whole}.{frac:06}")
}
}
impl fmt::Debug for Trx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Trx({} sun)", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sun(value: i64) -> Trx {
Trx::from_sun(value).unwrap()
}
#[test]
fn conversions() {
assert_eq!("1".parse::<Trx>().unwrap().as_sun(), 1_000_000);
assert_eq!("1.5".parse::<Trx>().unwrap().as_sun(), 1_500_000);
}
#[test]
fn rejects_negative() {
assert!(Trx::from_sun(-1).is_err());
assert!("-1".parse::<Trx>().is_err());
}
#[test]
fn unchecked_allows_negative() {
assert_eq!(Trx::from_sun_unchecked(-5).as_sun(), -5);
}
#[test]
fn arithmetic() {
let a = "1".parse::<Trx>().unwrap();
let b = "0.5".parse::<Trx>().unwrap();
assert_eq!((a + b).as_sun(), 1_500_000);
assert_eq!((a - b).as_sun(), 500_000);
assert_eq!(a.checked_add(b), Some(Trx::from_sun(1_500_000).unwrap()));
}
#[test]
fn parse_valid() {
assert_eq!("1".parse::<Trx>().unwrap().as_sun(), 1_000_000);
assert_eq!("1.5".parse::<Trx>().unwrap().as_sun(), 1_500_000);
assert_eq!(".5".parse::<Trx>().unwrap().as_sun(), 500_000);
assert_eq!("0.000001".parse::<Trx>().unwrap().as_sun(), 1);
assert_eq!("100".parse::<Trx>().unwrap().as_sun(), 100_000_000);
assert_eq!("1.000000".parse::<Trx>().unwrap().as_sun(), 1_000_000);
assert_eq!("1_000".parse::<Trx>().unwrap().as_sun(), 1_000_000_000);
assert_eq!("1.".parse::<Trx>().unwrap().as_sun(), 1_000_000);
assert_eq!("".parse::<Trx>().unwrap(), Trx::ZERO);
}
#[test]
fn parse_invalid() {
assert!("-1".parse::<Trx>().is_err());
assert!("abc".parse::<Trx>().is_err());
assert!("1.abc".parse::<Trx>().is_err());
assert!(" 1 ".parse::<Trx>().is_err());
assert!("+1".parse::<Trx>().is_err());
assert!("1.金额".parse::<Trx>().is_err());
}
#[test]
fn parse_truncates_beyond_sun_precision() {
assert_eq!("1.1234567".parse::<Trx>().unwrap().as_sun(), 1_123_456);
assert_eq!("0.0000009".parse::<Trx>().unwrap(), Trx::ZERO);
}
#[test]
fn display_is_exact() {
assert_eq!(sun(1_500_000).to_string(), "1.500000");
assert_eq!("100".parse::<Trx>().unwrap().to_string(), "100.000000");
assert_eq!(sun(1).to_string(), "0.000001");
assert_eq!(Trx::ZERO.to_string(), "0.000000");
assert_eq!(Trx::from_sun_unchecked(-1_500_000).to_string(), "-1.500000");
}
#[test]
fn display_parse_round_trip() {
for &sun in &[0, 1, 1_000_000, 1_500_000, 100_000_000, 123_456] {
let t = Trx::from_sun(sun).unwrap();
assert_eq!(t.to_string().parse::<Trx>().unwrap(), t);
}
}
#[test]
fn alloy_style_helpers() {
assert_eq!(parse_trx("1.5").unwrap().as_sun(), 1_500_000);
assert_eq!(format_trx(sun(1_500_000)), "1.500000");
}
#[test]
fn matches_alloy_unit_helpers_within_tron_range() {
for input in ["", ".5", "1.", "1_000", "1.1234567", "9223372036854.775807"] {
let alloy = alloy_primitives::utils::parse_units(input, 6).unwrap();
let expected = u64::try_from(alloy).unwrap();
assert_eq!(input.parse::<Trx>().unwrap().as_sun(), expected as i64);
}
for amount in [Trx::ZERO, sun(1), sun(1_500_000), "100".parse().unwrap()] {
let alloy = alloy_primitives::utils::format_units(amount.as_sun(), 6).unwrap();
assert_eq!(amount.to_string(), alloy);
}
}
#[test]
fn parse_accepts_max_i64_sun() {
let max = "9223372036854.775807".parse::<Trx>().unwrap();
assert_eq!(max.as_sun(), i64::MAX);
}
#[test]
fn parse_rejects_above_max_i64_sun() {
assert!("9223372036854.775808".parse::<Trx>().is_err());
}
#[test]
fn checked_sub_rejects_negative() {
assert!(Trx::ZERO.checked_sub(sun(1)).is_none());
}
#[test]
fn checked_arithmetic_rejects_negative_operands() {
let negative = Trx::from_sun_unchecked(-5);
assert!(sun(10).checked_add(negative).is_none());
assert!(negative.checked_add(sun(10)).is_none());
assert!(sun(10).checked_sub(negative).is_none());
assert!(negative.checked_sub(sun(10)).is_none());
}
}