Skip to main content

near_kit/types/
units.rs

1//! NEAR token amount and gas unit types.
2
3use std::fmt::{self, Display};
4use std::ops::{Add, Sub};
5use std::str::FromStr;
6
7use borsh::{BorshDeserialize, BorshSerialize};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use crate::error::{ParseAmountError, ParseGasError};
11
12/// One yoctoNEAR (10^-24 NEAR).
13const YOCTO_PER_NEAR: u128 = 1_000_000_000_000_000_000_000_000;
14/// One milliNEAR in yoctoNEAR (10^-3 NEAR = 10^21 yocto).
15const YOCTO_PER_MILLINEAR: u128 = 1_000_000_000_000_000_000_000;
16
17/// A NEAR token amount with yoctoNEAR precision (10^-24 NEAR).
18///
19/// # Creating Amounts
20///
21/// Use the typed constructors for compile-time safety:
22///
23/// ```
24/// use near_kit::NearToken;
25///
26/// // Preferred: typed constructors (const, zero-cost)
27/// let five_near = NearToken::near(5);
28/// let half_near = NearToken::millinear(500);
29/// let one_yocto = NearToken::yocto(1);
30///
31/// // Also available: longer form
32/// let amount = NearToken::from_near(5);
33/// ```
34///
35/// # Parsing from Strings
36///
37/// String parsing is available for runtime input (CLI, config files):
38/// - `"5 NEAR"` or `"5 near"` - whole NEAR
39/// - `"1.5 NEAR"` - decimal NEAR
40/// - `"500 milliNEAR"` or `"500 mNEAR"` - milliNEAR
41/// - `"1000 yocto"` or `"1000 yoctoNEAR"` - yoctoNEAR
42///
43/// Raw numbers are NOT accepted to prevent unit confusion.
44///
45/// ```
46/// use near_kit::NearToken;
47///
48/// // For runtime/user input only
49/// let amount: NearToken = "5 NEAR".parse().unwrap();
50/// ```
51#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
52pub struct NearToken(u128);
53
54impl NearToken {
55    /// Zero NEAR.
56    pub const ZERO: Self = Self(0);
57    /// One yoctoNEAR.
58    pub const ONE_YOCTO: Self = Self(1);
59    /// One milliNEAR.
60    pub const ONE_MILLINEAR: Self = Self(YOCTO_PER_MILLINEAR);
61    /// One NEAR.
62    pub const ONE_NEAR: Self = Self(YOCTO_PER_NEAR);
63
64    // ========================================================================
65    // Short alias constructors (preferred)
66    // ========================================================================
67
68    /// Create from whole NEAR (short alias for `from_near`).
69    ///
70    /// # Example
71    ///
72    /// ```
73    /// use near_kit::NearToken;
74    ///
75    /// let amount = NearToken::near(5);
76    /// assert_eq!(amount, NearToken::from_near(5));
77    /// ```
78    pub const fn near(near: u128) -> Self {
79        Self(near * YOCTO_PER_NEAR)
80    }
81
82    /// Create from milliNEAR (short alias for `from_millinear`).
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// use near_kit::NearToken;
88    ///
89    /// let amount = NearToken::millinear(500); // 0.5 NEAR
90    /// assert_eq!(amount, NearToken::from_millinear(500));
91    /// ```
92    pub const fn millinear(millinear: u128) -> Self {
93        Self(millinear * YOCTO_PER_MILLINEAR)
94    }
95
96    /// Create from yoctoNEAR (short alias for `from_yoctonear`).
97    ///
98    /// # Example
99    ///
100    /// ```
101    /// use near_kit::NearToken;
102    ///
103    /// let amount = NearToken::yocto(1);
104    /// assert_eq!(amount, NearToken::ONE_YOCTO);
105    /// ```
106    pub const fn yocto(yocto: u128) -> Self {
107        Self(yocto)
108    }
109
110    // ========================================================================
111    // Full-name constructors
112    // ========================================================================
113
114    /// Create from yoctoNEAR (10^-24 NEAR).
115    pub const fn from_yoctonear(yocto: u128) -> Self {
116        Self(yocto)
117    }
118
119    /// Create from milliNEAR (10^-3 NEAR).
120    pub const fn from_millinear(millinear: u128) -> Self {
121        Self(millinear * YOCTO_PER_MILLINEAR)
122    }
123
124    /// Create from whole NEAR.
125    pub const fn from_near(near: u128) -> Self {
126        Self(near * YOCTO_PER_NEAR)
127    }
128
129    /// Parse from decimal NEAR (e.g., 1.5 NEAR).
130    pub fn from_near_decimal(s: &str) -> Result<Self, ParseAmountError> {
131        let s = s.trim();
132
133        if let Some(dot_pos) = s.find('.') {
134            // Decimal NEAR
135            let integer_part = &s[..dot_pos];
136            let decimal_part = &s[dot_pos + 1..];
137
138            // Parse integer part
139            let integer: u128 = if integer_part.is_empty() {
140                0
141            } else {
142                integer_part
143                    .parse()
144                    .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?
145            };
146
147            // Parse decimal part (pad or truncate to 24 digits)
148            let decimal_str = if decimal_part.len() > 24 {
149                &decimal_part[..24]
150            } else {
151                decimal_part
152            };
153
154            let decimal: u128 = if decimal_str.is_empty() {
155                0
156            } else {
157                decimal_str
158                    .parse()
159                    .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?
160            };
161
162            // Scale the decimal part
163            let decimal_scale = 24 - decimal_str.len();
164            let decimal_yocto = decimal * 10u128.pow(decimal_scale as u32);
165
166            let total = integer
167                .checked_mul(YOCTO_PER_NEAR)
168                .and_then(|v| v.checked_add(decimal_yocto))
169                .ok_or(ParseAmountError::Overflow)?;
170
171            Ok(Self(total))
172        } else {
173            // Whole NEAR
174            let near: u128 = s
175                .parse()
176                .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?;
177            near.checked_mul(YOCTO_PER_NEAR)
178                .map(Self)
179                .ok_or(ParseAmountError::Overflow)
180        }
181    }
182
183    /// Get the raw yoctoNEAR value.
184    pub const fn as_yoctonear(&self) -> u128 {
185        self.0
186    }
187
188    /// Get the value as NEAR (may lose precision).
189    pub fn as_near_f64(&self) -> f64 {
190        self.0 as f64 / YOCTO_PER_NEAR as f64
191    }
192
193    /// Get whole NEAR (truncated).
194    pub const fn as_near(&self) -> u128 {
195        self.0 / YOCTO_PER_NEAR
196    }
197
198    /// Checked addition.
199    pub fn checked_add(self, other: Self) -> Option<Self> {
200        self.0.checked_add(other.0).map(Self)
201    }
202
203    /// Checked subtraction.
204    pub fn checked_sub(self, other: Self) -> Option<Self> {
205        self.0.checked_sub(other.0).map(Self)
206    }
207
208    /// Saturating addition.
209    pub fn saturating_add(self, other: Self) -> Self {
210        Self(self.0.saturating_add(other.0))
211    }
212
213    /// Saturating subtraction.
214    pub fn saturating_sub(self, other: Self) -> Self {
215        Self(self.0.saturating_sub(other.0))
216    }
217
218    /// Check if zero.
219    pub const fn is_zero(&self) -> bool {
220        self.0 == 0
221    }
222}
223
224impl FromStr for NearToken {
225    type Err = ParseAmountError;
226
227    fn from_str(s: &str) -> Result<Self, Self::Err> {
228        let s = s.trim();
229
230        // "X NEAR" or "X near"
231        if let Some(value) = s.strip_suffix(" NEAR").or_else(|| s.strip_suffix(" near")) {
232            return Self::from_near_decimal(value.trim());
233        }
234
235        // "X milliNEAR" or "X mNEAR"
236        if let Some(value) = s
237            .strip_suffix(" milliNEAR")
238            .or_else(|| s.strip_suffix(" mNEAR"))
239        {
240            let v: u128 = value
241                .trim()
242                .parse()
243                .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?;
244            return v
245                .checked_mul(YOCTO_PER_MILLINEAR)
246                .map(Self)
247                .ok_or(ParseAmountError::Overflow);
248        }
249
250        // "X yocto" or "X yoctoNEAR"
251        if let Some(value) = s
252            .strip_suffix(" yoctoNEAR")
253            .or_else(|| s.strip_suffix(" yocto"))
254        {
255            let v: u128 = value
256                .trim()
257                .parse()
258                .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?;
259            return Ok(Self(v));
260        }
261
262        // Bare number = error (ambiguous)
263        if s.chars().all(|c| c.is_ascii_digit() || c == '.') {
264            return Err(ParseAmountError::AmbiguousAmount(s.to_string()));
265        }
266
267        Err(ParseAmountError::InvalidFormat(s.to_string()))
268    }
269}
270
271impl TryFrom<&str> for NearToken {
272    type Error = ParseAmountError;
273
274    fn try_from(s: &str) -> Result<Self, Self::Error> {
275        s.parse()
276    }
277}
278
279impl Display for NearToken {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        if self.0 == 0 {
282            return write!(f, "0 NEAR");
283        }
284
285        let near = self.0 / YOCTO_PER_NEAR;
286        let remainder = self.0 % YOCTO_PER_NEAR;
287
288        if remainder == 0 {
289            write!(f, "{} NEAR", near)
290        } else {
291            // Show up to 5 decimal places, trim trailing zeros
292            let decimal = format!("{:024}", remainder);
293            let decimal = decimal.trim_end_matches('0');
294            let decimal_len = decimal.len().min(5);
295            write!(f, "{}.{} NEAR", near, &decimal[..decimal_len])
296        }
297    }
298}
299
300impl Add for NearToken {
301    type Output = Self;
302
303    fn add(self, other: Self) -> Self {
304        Self(self.0 + other.0)
305    }
306}
307
308impl Sub for NearToken {
309    type Output = Self;
310
311    fn sub(self, other: Self) -> Self {
312        Self(self.0 - other.0)
313    }
314}
315
316// Serde: serialize as string (yoctoNEAR) for JSON compatibility with NEAR RPC
317impl Serialize for NearToken {
318    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
319        s.serialize_str(&self.0.to_string())
320    }
321}
322
323impl<'de> Deserialize<'de> for NearToken {
324    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
325        let s: String = serde::Deserialize::deserialize(d)?;
326        Ok(Self(s.parse().map_err(serde::de::Error::custom)?))
327    }
328}
329
330impl BorshSerialize for NearToken {
331    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
332        borsh::BorshSerialize::serialize(&self.0, writer)
333    }
334}
335
336impl BorshDeserialize for NearToken {
337    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
338        Ok(Self(u128::deserialize_reader(reader)?))
339    }
340}
341
342// ============================================================================
343// Gas
344// ============================================================================
345
346/// Gas per petagas.
347const GAS_PER_PGAS: u64 = 1_000_000_000_000_000;
348/// Gas per teragas.
349const GAS_PER_TGAS: u64 = 1_000_000_000_000;
350/// Gas per gigagas.
351const GAS_PER_GGAS: u64 = 1_000_000_000;
352
353/// Gas units for NEAR transactions.
354///
355/// # Creating Gas Amounts
356///
357/// Use the typed constructors for compile-time safety:
358///
359/// ```
360/// use near_kit::Gas;
361///
362/// // Preferred: short aliases (const, zero-cost)
363/// let default_gas = Gas::tgas(30);
364/// let more_gas = Gas::tgas(100);
365///
366/// // Common constants
367/// let default = Gas::DEFAULT;  // 30 Tgas
368/// let max = Gas::MAX;          // 1 Pgas (1000 Tgas)
369/// ```
370///
371/// # Parsing from Strings
372///
373/// String parsing is available for runtime input:
374/// - `"30 Tgas"` or `"30 tgas"` - teragas (10^12)
375/// - `"5 Ggas"` or `"5 ggas"` - gigagas (10^9)
376/// - `"1000000 gas"` - raw gas units
377///
378/// ```
379/// use near_kit::Gas;
380///
381/// // For runtime/user input only
382/// let gas: Gas = "30 Tgas".parse().unwrap();
383/// assert_eq!(gas.as_gas(), 30_000_000_000_000);
384/// ```
385#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
386pub struct Gas(u64);
387
388impl Gas {
389    /// Zero gas.
390    pub const ZERO: Self = Self(0);
391    /// One gigagas (10^9).
392    pub const ONE_GGAS: Self = Self(GAS_PER_GGAS);
393    /// One teragas (10^12).
394    pub const ONE_TGAS: Self = Self(GAS_PER_TGAS);
395    /// One petagas (10^15).
396    pub const ONE_PGAS: Self = Self(GAS_PER_PGAS);
397
398    /// Default gas for function calls (30 Tgas).
399    pub const DEFAULT: Self = Self::from_tgas(30);
400
401    /// Maximum gas per transaction (1 Pgas / 1000 Tgas).
402    pub const MAX: Self = Self::from_tgas(1_000);
403
404    // ========================================================================
405    // Short alias constructors (preferred)
406    // ========================================================================
407
408    /// Create from teragas (short alias for `from_tgas`).
409    ///
410    /// # Example
411    ///
412    /// ```
413    /// use near_kit::Gas;
414    ///
415    /// let gas = Gas::tgas(30);
416    /// assert_eq!(gas, Gas::DEFAULT);
417    /// ```
418    pub const fn tgas(tgas: u64) -> Self {
419        Self(tgas * GAS_PER_TGAS)
420    }
421
422    /// Create from gigagas (short alias for `from_ggas`).
423    ///
424    /// # Example
425    ///
426    /// ```
427    /// use near_kit::Gas;
428    ///
429    /// let gas = Gas::ggas(5);
430    /// assert_eq!(gas.as_ggas(), 5);
431    /// ```
432    pub const fn ggas(ggas: u64) -> Self {
433        Self(ggas * GAS_PER_GGAS)
434    }
435
436    // ========================================================================
437    // Full-name constructors
438    // ========================================================================
439
440    /// Create from raw gas units.
441    pub const fn from_gas(gas: u64) -> Self {
442        Self(gas)
443    }
444
445    /// Create from gigagas (10^9).
446    pub const fn from_ggas(ggas: u64) -> Self {
447        Self(ggas * GAS_PER_GGAS)
448    }
449
450    /// Create from teragas (10^12).
451    pub const fn from_tgas(tgas: u64) -> Self {
452        Self(tgas * GAS_PER_TGAS)
453    }
454
455    /// Get raw gas value.
456    pub const fn as_gas(&self) -> u64 {
457        self.0
458    }
459
460    /// Get value in teragas (truncated).
461    pub const fn as_tgas(&self) -> u64 {
462        self.0 / GAS_PER_TGAS
463    }
464
465    /// Get value in gigagas (truncated).
466    pub const fn as_ggas(&self) -> u64 {
467        self.0 / GAS_PER_GGAS
468    }
469
470    /// Checked addition.
471    pub fn checked_add(self, other: Self) -> Option<Self> {
472        self.0.checked_add(other.0).map(Self)
473    }
474
475    /// Checked subtraction.
476    pub fn checked_sub(self, other: Self) -> Option<Self> {
477        self.0.checked_sub(other.0).map(Self)
478    }
479
480    /// Check if zero.
481    pub const fn is_zero(&self) -> bool {
482        self.0 == 0
483    }
484}
485
486impl FromStr for Gas {
487    type Err = ParseGasError;
488
489    fn from_str(s: &str) -> Result<Self, Self::Err> {
490        let s = s.trim();
491
492        // "X Tgas" or "X tgas" or "X TGas"
493        if let Some(value) = s
494            .strip_suffix(" Tgas")
495            .or_else(|| s.strip_suffix(" tgas"))
496            .or_else(|| s.strip_suffix(" TGas"))
497        {
498            let v: u64 = value
499                .trim()
500                .parse()
501                .map_err(|_| ParseGasError::InvalidNumber(s.to_string()))?;
502            return v
503                .checked_mul(GAS_PER_TGAS)
504                .map(Self)
505                .ok_or(ParseGasError::Overflow);
506        }
507
508        // "X Ggas" or "X ggas" or "X GGas"
509        if let Some(value) = s
510            .strip_suffix(" Ggas")
511            .or_else(|| s.strip_suffix(" ggas"))
512            .or_else(|| s.strip_suffix(" GGas"))
513        {
514            let v: u64 = value
515                .trim()
516                .parse()
517                .map_err(|_| ParseGasError::InvalidNumber(s.to_string()))?;
518            return v
519                .checked_mul(GAS_PER_GGAS)
520                .map(Self)
521                .ok_or(ParseGasError::Overflow);
522        }
523
524        // "X gas"
525        if let Some(value) = s.strip_suffix(" gas") {
526            let v: u64 = value
527                .trim()
528                .parse()
529                .map_err(|_| ParseGasError::InvalidNumber(s.to_string()))?;
530            return Ok(Self(v));
531        }
532
533        Err(ParseGasError::InvalidFormat(s.to_string()))
534    }
535}
536
537impl TryFrom<&str> for Gas {
538    type Error = ParseGasError;
539
540    fn try_from(s: &str) -> Result<Self, Self::Error> {
541        s.parse()
542    }
543}
544
545impl Display for Gas {
546    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547        let tgas = self.0 / GAS_PER_TGAS;
548        if tgas > 0 && self.0 % GAS_PER_TGAS == 0 {
549            write!(f, "{} Tgas", tgas)
550        } else {
551            write!(f, "{} gas", self.0)
552        }
553    }
554}
555
556impl Add for Gas {
557    type Output = Self;
558
559    fn add(self, other: Self) -> Self {
560        Self(self.0 + other.0)
561    }
562}
563
564impl Sub for Gas {
565    type Output = Self;
566
567    fn sub(self, other: Self) -> Self {
568        Self(self.0 - other.0)
569    }
570}
571
572impl Serialize for Gas {
573    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
574        s.serialize_u64(self.0)
575    }
576}
577
578impl<'de> Deserialize<'de> for Gas {
579    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
580        let v: u64 = serde::Deserialize::deserialize(d)?;
581        Ok(Self(v))
582    }
583}
584
585impl BorshSerialize for Gas {
586    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
587        borsh::BorshSerialize::serialize(&self.0, writer)
588    }
589}
590
591impl BorshDeserialize for Gas {
592    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
593        Ok(Self(u64::deserialize_reader(reader)?))
594    }
595}
596
597// ============================================================================
598// IntoNearToken trait
599// ============================================================================
600
601/// Trait for types that can be converted into a NearToken.
602///
603/// This allows methods to accept both typed NearToken values (preferred)
604/// and string representations for runtime input.
605///
606/// # Example
607///
608/// ```
609/// use near_kit::{IntoNearToken, NearToken};
610///
611/// fn example(amount: impl IntoNearToken) {
612///     let token = amount.into_near_token().unwrap();
613/// }
614///
615/// // Preferred: typed constructor
616/// example(NearToken::near(5));
617///
618/// // Also works: string parsing (for runtime input)
619/// example("5 NEAR");
620/// ```
621pub trait IntoNearToken {
622    /// Convert into a NearToken.
623    fn into_near_token(self) -> Result<NearToken, ParseAmountError>;
624}
625
626impl IntoNearToken for NearToken {
627    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
628        Ok(self)
629    }
630}
631
632impl IntoNearToken for &str {
633    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
634        self.parse()
635    }
636}
637
638impl IntoNearToken for String {
639    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
640        self.parse()
641    }
642}
643
644impl IntoNearToken for &String {
645    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
646        self.parse()
647    }
648}
649
650// ============================================================================
651// IntoGas trait
652// ============================================================================
653
654/// Trait for types that can be converted into Gas.
655///
656/// This allows methods to accept both typed Gas values (preferred)
657/// and string representations for runtime input.
658///
659/// # Example
660///
661/// ```
662/// use near_kit::{Gas, IntoGas};
663///
664/// fn example(gas: impl IntoGas) {
665///     let g = gas.into_gas().unwrap();
666/// }
667///
668/// // Preferred: typed constructor
669/// example(Gas::tgas(30));
670///
671/// // Also works: string parsing (for runtime input)
672/// example("30 Tgas");
673/// ```
674pub trait IntoGas {
675    /// Convert into Gas.
676    fn into_gas(self) -> Result<Gas, ParseGasError>;
677}
678
679impl IntoGas for Gas {
680    fn into_gas(self) -> Result<Gas, ParseGasError> {
681        Ok(self)
682    }
683}
684
685impl IntoGas for &str {
686    fn into_gas(self) -> Result<Gas, ParseGasError> {
687        self.parse()
688    }
689}
690
691impl IntoGas for String {
692    fn into_gas(self) -> Result<Gas, ParseGasError> {
693        self.parse()
694    }
695}
696
697impl IntoGas for &String {
698    fn into_gas(self) -> Result<Gas, ParseGasError> {
699        self.parse()
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    // ========================================================================
708    // NearToken parsing tests
709    // ========================================================================
710
711    #[test]
712    fn test_near_token_parsing() {
713        assert_eq!(
714            "5 NEAR".parse::<NearToken>().unwrap().as_yoctonear(),
715            5 * YOCTO_PER_NEAR
716        );
717        assert_eq!(
718            "1.5 NEAR".parse::<NearToken>().unwrap().as_yoctonear(),
719            YOCTO_PER_NEAR + YOCTO_PER_NEAR / 2
720        );
721        assert_eq!(
722            "100 milliNEAR".parse::<NearToken>().unwrap().as_yoctonear(),
723            100 * YOCTO_PER_MILLINEAR
724        );
725        assert_eq!(
726            "1000 yocto".parse::<NearToken>().unwrap().as_yoctonear(),
727            1000
728        );
729    }
730
731    #[test]
732    fn test_near_token_display() {
733        assert_eq!(NearToken::ZERO.to_string(), "0 NEAR");
734        assert_eq!(NearToken::from_near(5).to_string(), "5 NEAR");
735        assert_eq!(NearToken::from_near(100).to_string(), "100 NEAR");
736    }
737
738    #[test]
739    fn test_near_token_ambiguous() {
740        assert!(matches!(
741            "123".parse::<NearToken>(),
742            Err(ParseAmountError::AmbiguousAmount(_))
743        ));
744    }
745
746    #[test]
747    fn test_gas_parsing() {
748        assert_eq!(
749            "30 Tgas".parse::<Gas>().unwrap().as_gas(),
750            30 * GAS_PER_TGAS
751        );
752        assert_eq!("5 Ggas".parse::<Gas>().unwrap().as_gas(), 5 * GAS_PER_GGAS);
753        assert_eq!("1000 gas".parse::<Gas>().unwrap().as_gas(), 1000);
754    }
755
756    #[test]
757    fn test_gas_display() {
758        assert_eq!(Gas::from_tgas(30).to_string(), "30 Tgas");
759        assert_eq!(Gas::from_gas(1000).to_string(), "1000 gas");
760    }
761
762    #[test]
763    fn test_gas_default() {
764        assert_eq!(Gas::DEFAULT.as_tgas(), 30);
765    }
766
767    // ========================================================================
768    // NearToken constructor tests
769    // ========================================================================
770
771    #[test]
772    fn test_near_token_constructors() {
773        // Short aliases
774        assert_eq!(NearToken::near(5).as_yoctonear(), 5 * YOCTO_PER_NEAR);
775        assert_eq!(
776            NearToken::millinear(500).as_yoctonear(),
777            500 * YOCTO_PER_MILLINEAR
778        );
779        assert_eq!(NearToken::yocto(1000).as_yoctonear(), 1000);
780
781        // Full names
782        assert_eq!(NearToken::from_near(5).as_yoctonear(), 5 * YOCTO_PER_NEAR);
783        assert_eq!(
784            NearToken::from_millinear(500).as_yoctonear(),
785            500 * YOCTO_PER_MILLINEAR
786        );
787        assert_eq!(NearToken::from_yoctonear(1000).as_yoctonear(), 1000);
788    }
789
790    #[test]
791    fn test_near_token_constants() {
792        assert_eq!(NearToken::ZERO.as_yoctonear(), 0);
793        assert_eq!(NearToken::ONE_YOCTO.as_yoctonear(), 1);
794        assert_eq!(NearToken::ONE_MILLINEAR.as_yoctonear(), YOCTO_PER_MILLINEAR);
795        assert_eq!(NearToken::ONE_NEAR.as_yoctonear(), YOCTO_PER_NEAR);
796    }
797
798    #[test]
799    fn test_near_token_as_near() {
800        assert_eq!(NearToken::near(5).as_near(), 5);
801        assert_eq!(NearToken::millinear(500).as_near(), 0); // Truncated
802        assert_eq!(NearToken::millinear(1500).as_near(), 1); // Truncated
803    }
804
805    #[test]
806    fn test_near_token_as_near_f64() {
807        let amount = NearToken::millinear(500);
808        let f64_val = amount.as_near_f64();
809        assert!((f64_val - 0.5).abs() < 0.0001);
810    }
811
812    #[test]
813    fn test_near_token_is_zero() {
814        assert!(NearToken::ZERO.is_zero());
815        assert!(!NearToken::ONE_YOCTO.is_zero());
816    }
817
818    // ========================================================================
819    // NearToken arithmetic tests
820    // ========================================================================
821
822    #[test]
823    fn test_near_token_add() {
824        let a = NearToken::near(5);
825        let b = NearToken::near(3);
826        assert_eq!((a + b).as_near(), 8);
827    }
828
829    #[test]
830    fn test_near_token_sub() {
831        let a = NearToken::near(5);
832        let b = NearToken::near(3);
833        assert_eq!((a - b).as_near(), 2);
834    }
835
836    #[test]
837    fn test_near_token_checked_add() {
838        let a = NearToken::near(5);
839        let b = NearToken::near(3);
840        assert_eq!(a.checked_add(b).unwrap().as_near(), 8);
841
842        // Overflow
843        let max = NearToken::from_yoctonear(u128::MAX);
844        assert!(max.checked_add(NearToken::ONE_YOCTO).is_none());
845    }
846
847    #[test]
848    fn test_near_token_checked_sub() {
849        let a = NearToken::near(5);
850        let b = NearToken::near(3);
851        assert_eq!(a.checked_sub(b).unwrap().as_near(), 2);
852
853        // Underflow
854        assert!(b.checked_sub(a).is_none());
855    }
856
857    #[test]
858    fn test_near_token_saturating_add() {
859        let a = NearToken::near(5);
860        let b = NearToken::near(3);
861        assert_eq!(a.saturating_add(b).as_near(), 8);
862
863        // Saturates at max
864        let max = NearToken::from_yoctonear(u128::MAX);
865        assert_eq!(max.saturating_add(NearToken::ONE_YOCTO), max);
866    }
867
868    #[test]
869    fn test_near_token_saturating_sub() {
870        let a = NearToken::near(5);
871        let b = NearToken::near(3);
872        assert_eq!(a.saturating_sub(b).as_near(), 2);
873
874        // Saturates at zero
875        assert_eq!(b.saturating_sub(a), NearToken::ZERO);
876    }
877
878    // ========================================================================
879    // NearToken parsing edge cases
880    // ========================================================================
881
882    #[test]
883    fn test_near_token_parse_lowercase() {
884        assert_eq!("5 near".parse::<NearToken>().unwrap().as_near(), 5);
885    }
886
887    #[test]
888    fn test_near_token_parse_mnear() {
889        assert_eq!(
890            "100 mNEAR".parse::<NearToken>().unwrap().as_yoctonear(),
891            100 * YOCTO_PER_MILLINEAR
892        );
893    }
894
895    #[test]
896    fn test_near_token_parse_yoctonear() {
897        assert_eq!(
898            "12345 yoctoNEAR"
899                .parse::<NearToken>()
900                .unwrap()
901                .as_yoctonear(),
902            12345
903        );
904    }
905
906    #[test]
907    fn test_near_token_parse_decimal_near() {
908        assert_eq!(
909            "0.5 NEAR".parse::<NearToken>().unwrap().as_yoctonear(),
910            YOCTO_PER_NEAR / 2
911        );
912        assert_eq!(
913            ".25 NEAR".parse::<NearToken>().unwrap().as_yoctonear(),
914            YOCTO_PER_NEAR / 4
915        );
916    }
917
918    #[test]
919    fn test_near_token_parse_with_whitespace() {
920        assert_eq!("  5 NEAR  ".parse::<NearToken>().unwrap().as_near(), 5);
921    }
922
923    #[test]
924    fn test_near_token_parse_invalid_format() {
925        assert!(matches!(
926            "5 ETH".parse::<NearToken>(),
927            Err(ParseAmountError::InvalidFormat(_))
928        ));
929    }
930
931    #[test]
932    fn test_near_token_parse_invalid_number() {
933        assert!(matches!(
934            "abc NEAR".parse::<NearToken>(),
935            Err(ParseAmountError::InvalidNumber(_))
936        ));
937    }
938
939    #[test]
940    fn test_near_token_try_from_str() {
941        let token = NearToken::try_from("5 NEAR").unwrap();
942        assert_eq!(token.as_near(), 5);
943    }
944
945    // ========================================================================
946    // NearToken serde tests
947    // ========================================================================
948
949    #[test]
950    fn test_near_token_serde_roundtrip() {
951        let amount = NearToken::near(5);
952        let json = serde_json::to_string(&amount).unwrap();
953        // Should serialize as string (yoctoNEAR)
954        assert_eq!(json, format!("\"{}\"", amount.as_yoctonear()));
955
956        let parsed: NearToken = serde_json::from_str(&json).unwrap();
957        assert_eq!(amount, parsed);
958    }
959
960    #[test]
961    fn test_near_token_borsh_roundtrip() {
962        let amount = NearToken::near(10);
963        let bytes = borsh::to_vec(&amount).unwrap();
964        let parsed: NearToken = borsh::from_slice(&bytes).unwrap();
965        assert_eq!(amount, parsed);
966    }
967
968    // ========================================================================
969    // NearToken display tests (fractional)
970    // ========================================================================
971
972    #[test]
973    fn test_near_token_display_fractional() {
974        // 1.5 NEAR
975        let amount = NearToken::from_yoctonear(YOCTO_PER_NEAR + YOCTO_PER_NEAR / 2);
976        let display = amount.to_string();
977        assert!(display.contains("1.5") || display.contains("1."));
978        assert!(display.contains("NEAR"));
979    }
980
981    // ========================================================================
982    // NearToken comparison tests
983    // ========================================================================
984
985    #[test]
986    fn test_near_token_ord() {
987        let small = NearToken::near(1);
988        let large = NearToken::near(10);
989        assert!(small < large);
990        assert!(large > small);
991        assert!(small <= small);
992        assert!(small >= small);
993    }
994
995    #[test]
996    fn test_near_token_eq() {
997        let a = NearToken::near(5);
998        let b = NearToken::millinear(5000);
999        assert_eq!(a, b);
1000    }
1001
1002    #[test]
1003    fn test_near_token_hash() {
1004        use std::collections::HashSet;
1005        let mut set = HashSet::new();
1006        set.insert(NearToken::near(1));
1007        set.insert(NearToken::near(2));
1008        assert!(set.contains(&NearToken::near(1)));
1009        assert!(!set.contains(&NearToken::near(3)));
1010    }
1011
1012    // ========================================================================
1013    // Gas tests
1014    // ========================================================================
1015
1016    #[test]
1017    fn test_gas_constructors() {
1018        assert_eq!(Gas::tgas(30).as_gas(), 30 * GAS_PER_TGAS);
1019        assert_eq!(Gas::ggas(5).as_gas(), 5 * GAS_PER_GGAS);
1020        assert_eq!(Gas::from_gas(1000).as_gas(), 1000);
1021        assert_eq!(Gas::from_tgas(30).as_gas(), 30 * GAS_PER_TGAS);
1022        assert_eq!(Gas::from_ggas(5).as_gas(), 5 * GAS_PER_GGAS);
1023    }
1024
1025    #[test]
1026    fn test_gas_constants() {
1027        assert_eq!(Gas::ZERO.as_gas(), 0);
1028        assert_eq!(Gas::ONE_GGAS.as_gas(), GAS_PER_GGAS);
1029        assert_eq!(Gas::ONE_TGAS.as_gas(), GAS_PER_TGAS);
1030        assert_eq!(Gas::ONE_PGAS.as_gas(), GAS_PER_PGAS);
1031        assert_eq!(Gas::DEFAULT.as_tgas(), 30);
1032        assert_eq!(Gas::MAX.as_tgas(), 1_000);
1033    }
1034
1035    #[test]
1036    fn test_gas_as_accessors() {
1037        let gas = Gas::tgas(30);
1038        assert_eq!(gas.as_tgas(), 30);
1039        assert_eq!(gas.as_ggas(), 30_000);
1040        assert_eq!(gas.as_gas(), 30 * GAS_PER_TGAS);
1041    }
1042
1043    #[test]
1044    fn test_gas_is_zero() {
1045        assert!(Gas::ZERO.is_zero());
1046        assert!(!Gas::ONE_GGAS.is_zero());
1047    }
1048
1049    #[test]
1050    fn test_gas_add() {
1051        let a = Gas::tgas(10);
1052        let b = Gas::tgas(20);
1053        assert_eq!((a + b).as_tgas(), 30);
1054    }
1055
1056    #[test]
1057    fn test_gas_sub() {
1058        let a = Gas::tgas(30);
1059        let b = Gas::tgas(10);
1060        assert_eq!((a - b).as_tgas(), 20);
1061    }
1062
1063    #[test]
1064    fn test_gas_checked_add() {
1065        let a = Gas::tgas(10);
1066        let b = Gas::tgas(20);
1067        assert_eq!(a.checked_add(b).unwrap().as_tgas(), 30);
1068
1069        // Overflow
1070        let max = Gas::from_gas(u64::MAX);
1071        assert!(max.checked_add(Gas::from_gas(1)).is_none());
1072    }
1073
1074    #[test]
1075    fn test_gas_checked_sub() {
1076        let a = Gas::tgas(30);
1077        let b = Gas::tgas(10);
1078        assert_eq!(a.checked_sub(b).unwrap().as_tgas(), 20);
1079
1080        // Underflow
1081        assert!(b.checked_sub(a).is_none());
1082    }
1083
1084    #[test]
1085    fn test_gas_parse_tgas_variants() {
1086        assert_eq!("30 Tgas".parse::<Gas>().unwrap().as_tgas(), 30);
1087        assert_eq!("30 tgas".parse::<Gas>().unwrap().as_tgas(), 30);
1088        assert_eq!("30 TGas".parse::<Gas>().unwrap().as_tgas(), 30);
1089    }
1090
1091    #[test]
1092    fn test_gas_parse_ggas_variants() {
1093        assert_eq!("5 Ggas".parse::<Gas>().unwrap().as_ggas(), 5);
1094        assert_eq!("5 ggas".parse::<Gas>().unwrap().as_ggas(), 5);
1095        assert_eq!("5 GGas".parse::<Gas>().unwrap().as_ggas(), 5);
1096    }
1097
1098    #[test]
1099    fn test_gas_parse_invalid_format() {
1100        assert!(matches!(
1101            "30 teragas".parse::<Gas>(),
1102            Err(ParseGasError::InvalidFormat(_))
1103        ));
1104    }
1105
1106    #[test]
1107    fn test_gas_parse_invalid_number() {
1108        assert!(matches!(
1109            "abc Tgas".parse::<Gas>(),
1110            Err(ParseGasError::InvalidNumber(_))
1111        ));
1112    }
1113
1114    #[test]
1115    fn test_gas_try_from_str() {
1116        let gas = Gas::try_from("30 Tgas").unwrap();
1117        assert_eq!(gas.as_tgas(), 30);
1118    }
1119
1120    #[test]
1121    fn test_gas_serde_roundtrip() {
1122        let gas = Gas::tgas(30);
1123        let json = serde_json::to_string(&gas).unwrap();
1124        let parsed: Gas = serde_json::from_str(&json).unwrap();
1125        assert_eq!(gas, parsed);
1126    }
1127
1128    #[test]
1129    fn test_gas_borsh_roundtrip() {
1130        let gas = Gas::tgas(30);
1131        let bytes = borsh::to_vec(&gas).unwrap();
1132        let parsed: Gas = borsh::from_slice(&bytes).unwrap();
1133        assert_eq!(gas, parsed);
1134    }
1135
1136    #[test]
1137    fn test_gas_ord() {
1138        let small = Gas::tgas(10);
1139        let large = Gas::tgas(100);
1140        assert!(small < large);
1141    }
1142
1143    // ========================================================================
1144    // IntoNearToken tests
1145    // ========================================================================
1146
1147    #[test]
1148    fn test_into_near_token_from_near_token() {
1149        let token = NearToken::near(5);
1150        assert_eq!(token.into_near_token().unwrap(), NearToken::near(5));
1151    }
1152
1153    #[test]
1154    fn test_into_near_token_from_str() {
1155        assert_eq!("5 NEAR".into_near_token().unwrap(), NearToken::near(5));
1156    }
1157
1158    #[test]
1159    fn test_into_near_token_from_string() {
1160        let s = String::from("5 NEAR");
1161        assert_eq!(s.into_near_token().unwrap(), NearToken::near(5));
1162    }
1163
1164    #[test]
1165    fn test_into_near_token_from_string_ref() {
1166        let s = String::from("5 NEAR");
1167        assert_eq!((&s).into_near_token().unwrap(), NearToken::near(5));
1168    }
1169
1170    // ========================================================================
1171    // IntoGas tests
1172    // ========================================================================
1173
1174    #[test]
1175    fn test_into_gas_from_gas() {
1176        let gas = Gas::tgas(30);
1177        assert_eq!(gas.into_gas().unwrap(), Gas::tgas(30));
1178    }
1179
1180    #[test]
1181    fn test_into_gas_from_str() {
1182        assert_eq!("30 Tgas".into_gas().unwrap(), Gas::tgas(30));
1183    }
1184
1185    #[test]
1186    fn test_into_gas_from_string() {
1187        let s = String::from("30 Tgas");
1188        assert_eq!(s.into_gas().unwrap(), Gas::tgas(30));
1189    }
1190
1191    #[test]
1192    fn test_into_gas_from_string_ref() {
1193        let s = String::from("30 Tgas");
1194        assert_eq!((&s).into_gas().unwrap(), Gas::tgas(30));
1195    }
1196
1197    // ========================================================================
1198    // Edge case tests
1199    // ========================================================================
1200
1201    #[test]
1202    fn test_near_token_default() {
1203        let default = NearToken::default();
1204        assert_eq!(default, NearToken::ZERO);
1205    }
1206
1207    #[test]
1208    fn test_gas_default_trait() {
1209        let default = Gas::default();
1210        assert_eq!(default, Gas::ZERO);
1211    }
1212
1213    #[test]
1214    fn test_near_token_debug() {
1215        let token = NearToken::near(5);
1216        let debug = format!("{:?}", token);
1217        assert!(debug.contains("NearToken"));
1218    }
1219
1220    #[test]
1221    fn test_gas_debug() {
1222        let gas = Gas::tgas(30);
1223        let debug = format!("{:?}", gas);
1224        assert!(debug.contains("Gas"));
1225    }
1226
1227    #[test]
1228    fn test_gas_display_non_tgas_multiple() {
1229        // When gas is not a clean Tgas multiple
1230        let gas = Gas::from_gas(1500);
1231        assert_eq!(gas.to_string(), "1500 gas");
1232    }
1233}