Skip to main content

near_kit/types/
units.rs

1//! NEAR token amount and gas unit types — re-exported from upstream crates
2//! with near-kit ergonomic extensions.
3
4pub use near_gas::NearGas as Gas;
5pub use near_token::NearToken;
6
7use crate::error::{ParseAmountError, ParseGasError};
8
9// ============================================================================
10// Constants (used by parsing helpers)
11// ============================================================================
12
13/// One yoctoNEAR (10^-24 NEAR).
14const YOCTO_PER_NEAR: u128 = 1_000_000_000_000_000_000_000_000;
15/// One milliNEAR in yoctoNEAR (10^-3 NEAR = 10^21 yocto).
16const YOCTO_PER_MILLINEAR: u128 = 1_000_000_000_000_000_000_000;
17
18/// Gas per teragas.
19const GAS_PER_TGAS: u64 = 1_000_000_000_000;
20/// Gas per gigagas.
21const GAS_PER_GGAS: u64 = 1_000_000_000;
22
23// ============================================================================
24// IntoNearToken trait
25// ============================================================================
26
27/// Trait for types that can be converted into a NearToken.
28///
29/// This allows methods to accept both typed NearToken values (preferred)
30/// and string representations for runtime input.
31///
32/// # Example
33///
34/// ```
35/// use near_kit::{IntoNearToken, NearToken};
36///
37/// fn example(amount: impl IntoNearToken) {
38///     let token = amount.into_near_token().unwrap();
39/// }
40///
41/// // Preferred: typed constructor
42/// example(NearToken::from_near(5));
43///
44/// // Also works: string parsing (for runtime input)
45/// example("5 NEAR");
46/// ```
47pub trait IntoNearToken {
48    /// Convert into a NearToken.
49    fn into_near_token(self) -> Result<NearToken, ParseAmountError>;
50}
51
52impl IntoNearToken for NearToken {
53    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
54        Ok(self)
55    }
56}
57
58impl IntoNearToken for &str {
59    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
60        parse_near_token(self)
61    }
62}
63
64impl IntoNearToken for String {
65    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
66        parse_near_token(&self)
67    }
68}
69
70impl IntoNearToken for &String {
71    fn into_near_token(self) -> Result<NearToken, ParseAmountError> {
72        parse_near_token(self)
73    }
74}
75
76// ============================================================================
77// IntoGas trait
78// ============================================================================
79
80/// Trait for types that can be converted into Gas.
81///
82/// This allows methods to accept both typed Gas values (preferred)
83/// and string representations for runtime input.
84///
85/// # Example
86///
87/// ```
88/// use near_kit::{Gas, IntoGas};
89///
90/// fn example(gas: impl IntoGas) {
91///     let g = gas.into_gas().unwrap();
92/// }
93///
94/// // Preferred: typed constructor
95/// example(Gas::from_tgas(30));
96///
97/// // Also works: string parsing (for runtime input)
98/// example("30 Tgas");
99/// ```
100pub trait IntoGas {
101    /// Convert into Gas.
102    fn into_gas(self) -> Result<Gas, ParseGasError>;
103}
104
105impl IntoGas for Gas {
106    fn into_gas(self) -> Result<Gas, ParseGasError> {
107        Ok(self)
108    }
109}
110
111impl IntoGas for &str {
112    fn into_gas(self) -> Result<Gas, ParseGasError> {
113        parse_gas(self)
114    }
115}
116
117impl IntoGas for String {
118    fn into_gas(self) -> Result<Gas, ParseGasError> {
119        parse_gas(&self)
120    }
121}
122
123impl IntoGas for &String {
124    fn into_gas(self) -> Result<Gas, ParseGasError> {
125        parse_gas(self)
126    }
127}
128
129// ============================================================================
130// String parsing helpers (near-kit specific formats)
131// ============================================================================
132
133/// Parse a decimal NEAR string (e.g., "1.5") into yoctoNEAR.
134fn parse_near_decimal(s: &str) -> Result<NearToken, ParseAmountError> {
135    let s = s.trim();
136
137    if let Some(dot_pos) = s.find('.') {
138        let integer_part = &s[..dot_pos];
139        let decimal_part = &s[dot_pos + 1..];
140
141        let integer: u128 = if integer_part.is_empty() {
142            0
143        } else {
144            integer_part
145                .parse()
146                .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?
147        };
148
149        let decimal_str = if decimal_part.len() > 24 {
150            &decimal_part[..24]
151        } else {
152            decimal_part
153        };
154
155        let decimal: u128 = if decimal_str.is_empty() {
156            0
157        } else {
158            decimal_str
159                .parse()
160                .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?
161        };
162
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(NearToken::from_yoctonear(total))
172    } else {
173        let near: u128 = s
174            .parse()
175            .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?;
176        near.checked_mul(YOCTO_PER_NEAR)
177            .map(NearToken::from_yoctonear)
178            .ok_or(ParseAmountError::Overflow)
179    }
180}
181
182/// Parse a NearToken from a near-kit format string.
183///
184/// Supported formats:
185/// - `"5 NEAR"` or `"5 near"` — whole NEAR
186/// - `"1.5 NEAR"` — decimal NEAR
187/// - `"500 milliNEAR"` or `"500 mNEAR"` — milliNEAR
188/// - `"1000 yocto"` or `"1000 yoctoNEAR"` — yoctoNEAR
189///
190/// Raw numbers are NOT accepted to prevent unit confusion.
191pub fn parse_near_token(s: &str) -> Result<NearToken, ParseAmountError> {
192    let s = s.trim();
193
194    // "X NEAR" or "X near"
195    if let Some(value) = s.strip_suffix(" NEAR").or_else(|| s.strip_suffix(" near")) {
196        return parse_near_decimal(value.trim());
197    }
198
199    // "X milliNEAR" or "X mNEAR"
200    if let Some(value) = s
201        .strip_suffix(" milliNEAR")
202        .or_else(|| s.strip_suffix(" mNEAR"))
203    {
204        let v: u128 = value
205            .trim()
206            .parse()
207            .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?;
208        return v
209            .checked_mul(YOCTO_PER_MILLINEAR)
210            .map(NearToken::from_yoctonear)
211            .ok_or(ParseAmountError::Overflow);
212    }
213
214    // "X yocto" or "X yoctoNEAR"
215    if let Some(value) = s
216        .strip_suffix(" yoctoNEAR")
217        .or_else(|| s.strip_suffix(" yocto"))
218    {
219        let v: u128 = value
220            .trim()
221            .parse()
222            .map_err(|_| ParseAmountError::InvalidNumber(s.to_string()))?;
223        return Ok(NearToken::from_yoctonear(v));
224    }
225
226    // Bare number = error (ambiguous)
227    if s.chars().all(|c| c.is_ascii_digit() || c == '.') {
228        return Err(ParseAmountError::AmbiguousAmount(s.to_string()));
229    }
230
231    Err(ParseAmountError::InvalidFormat(s.to_string()))
232}
233
234/// Parse a Gas value from a near-kit format string.
235///
236/// Supported formats:
237/// - `"30 Tgas"` or `"30 tgas"` or `"30 TGas"` — teragas (10^12)
238/// - `"5 Ggas"` or `"5 ggas"` or `"5 GGas"` — gigagas (10^9)
239/// - `"1000000 gas"` — raw gas units
240pub fn parse_gas(s: &str) -> Result<Gas, ParseGasError> {
241    let s = s.trim();
242
243    // "X Tgas" or "X tgas" or "X TGas"
244    if let Some(value) = s
245        .strip_suffix(" Tgas")
246        .or_else(|| s.strip_suffix(" tgas"))
247        .or_else(|| s.strip_suffix(" TGas"))
248    {
249        let v: u64 = value
250            .trim()
251            .parse()
252            .map_err(|_| ParseGasError::InvalidNumber(s.to_string()))?;
253        return v
254            .checked_mul(GAS_PER_TGAS)
255            .map(Gas::from_gas)
256            .ok_or(ParseGasError::Overflow);
257    }
258
259    // "X Ggas" or "X ggas" or "X GGas"
260    if let Some(value) = s
261        .strip_suffix(" Ggas")
262        .or_else(|| s.strip_suffix(" ggas"))
263        .or_else(|| s.strip_suffix(" GGas"))
264    {
265        let v: u64 = value
266            .trim()
267            .parse()
268            .map_err(|_| ParseGasError::InvalidNumber(s.to_string()))?;
269        return v
270            .checked_mul(GAS_PER_GGAS)
271            .map(Gas::from_gas)
272            .ok_or(ParseGasError::Overflow);
273    }
274
275    // "X gas"
276    if let Some(value) = s.strip_suffix(" gas") {
277        let v: u64 = value
278            .trim()
279            .parse()
280            .map_err(|_| ParseGasError::InvalidNumber(s.to_string()))?;
281        return Ok(Gas::from_gas(v));
282    }
283
284    Err(ParseGasError::InvalidFormat(s.to_string()))
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    // ========================================================================
292    // NearToken parsing tests
293    // ========================================================================
294
295    #[test]
296    fn test_near_token_parsing() {
297        assert_eq!(
298            parse_near_token("5 NEAR").unwrap().as_yoctonear(),
299            5 * YOCTO_PER_NEAR
300        );
301        assert_eq!(
302            parse_near_token("1.5 NEAR").unwrap().as_yoctonear(),
303            YOCTO_PER_NEAR + YOCTO_PER_NEAR / 2
304        );
305        assert_eq!(
306            parse_near_token("100 milliNEAR").unwrap().as_yoctonear(),
307            100 * YOCTO_PER_MILLINEAR
308        );
309        assert_eq!(parse_near_token("1000 yocto").unwrap().as_yoctonear(), 1000);
310    }
311
312    #[test]
313    fn test_near_token_ambiguous() {
314        assert!(matches!(
315            parse_near_token("123"),
316            Err(ParseAmountError::AmbiguousAmount(_))
317        ));
318    }
319
320    #[test]
321    fn test_gas_parsing() {
322        assert_eq!(parse_gas("30 Tgas").unwrap().as_gas(), 30 * GAS_PER_TGAS);
323        assert_eq!(parse_gas("5 Ggas").unwrap().as_gas(), 5 * GAS_PER_GGAS);
324        assert_eq!(parse_gas("1000 gas").unwrap().as_gas(), 1000);
325    }
326
327    // ========================================================================
328    // NearToken constructor tests
329    // ========================================================================
330
331    #[test]
332    fn test_near_token_constructors() {
333        assert_eq!(NearToken::from_near(5).as_yoctonear(), 5 * YOCTO_PER_NEAR);
334        assert_eq!(
335            NearToken::from_millinear(500).as_yoctonear(),
336            500 * YOCTO_PER_MILLINEAR
337        );
338        assert_eq!(NearToken::from_yoctonear(1000).as_yoctonear(), 1000);
339    }
340
341    #[test]
342    fn test_near_token_as_near() {
343        assert_eq!(NearToken::from_near(5).as_near(), 5);
344        assert_eq!(NearToken::from_millinear(500).as_near(), 0); // Truncated
345        assert_eq!(NearToken::from_millinear(1500).as_near(), 1); // Truncated
346    }
347
348    #[test]
349    fn test_near_token_is_zero() {
350        assert!(NearToken::from_yoctonear(0).is_zero());
351        assert!(!NearToken::from_yoctonear(1).is_zero());
352    }
353
354    // ========================================================================
355    // NearToken arithmetic tests
356    // ========================================================================
357
358    #[test]
359    fn test_near_token_checked_add() {
360        let a = NearToken::from_near(5);
361        let b = NearToken::from_near(3);
362        assert_eq!(a.checked_add(b).unwrap().as_near(), 8);
363
364        // Overflow
365        let max = NearToken::from_yoctonear(u128::MAX);
366        assert!(max.checked_add(NearToken::from_yoctonear(1)).is_none());
367    }
368
369    #[test]
370    fn test_near_token_checked_sub() {
371        let a = NearToken::from_near(5);
372        let b = NearToken::from_near(3);
373        assert_eq!(a.checked_sub(b).unwrap().as_near(), 2);
374
375        // Underflow
376        assert!(b.checked_sub(a).is_none());
377    }
378
379    #[test]
380    fn test_near_token_saturating_add() {
381        let a = NearToken::from_near(5);
382        let b = NearToken::from_near(3);
383        assert_eq!(a.saturating_add(b).as_near(), 8);
384
385        // Saturates at max
386        let max = NearToken::from_yoctonear(u128::MAX);
387        assert_eq!(max.saturating_add(NearToken::from_yoctonear(1)), max);
388    }
389
390    #[test]
391    fn test_near_token_saturating_sub() {
392        let a = NearToken::from_near(5);
393        let b = NearToken::from_near(3);
394        assert_eq!(a.saturating_sub(b).as_near(), 2);
395
396        // Saturates at zero
397        assert_eq!(b.saturating_sub(a), NearToken::from_yoctonear(0));
398    }
399
400    // ========================================================================
401    // NearToken parsing edge cases
402    // ========================================================================
403
404    #[test]
405    fn test_near_token_parse_lowercase() {
406        assert_eq!(parse_near_token("5 near").unwrap().as_near(), 5);
407    }
408
409    #[test]
410    fn test_near_token_parse_mnear() {
411        assert_eq!(
412            parse_near_token("100 mNEAR").unwrap().as_yoctonear(),
413            100 * YOCTO_PER_MILLINEAR
414        );
415    }
416
417    #[test]
418    fn test_near_token_parse_yoctonear() {
419        assert_eq!(
420            parse_near_token("12345 yoctoNEAR").unwrap().as_yoctonear(),
421            12345
422        );
423    }
424
425    #[test]
426    fn test_near_token_parse_decimal_near() {
427        assert_eq!(
428            parse_near_token("0.5 NEAR").unwrap().as_yoctonear(),
429            YOCTO_PER_NEAR / 2
430        );
431        assert_eq!(
432            parse_near_token(".25 NEAR").unwrap().as_yoctonear(),
433            YOCTO_PER_NEAR / 4
434        );
435    }
436
437    #[test]
438    fn test_near_token_parse_with_whitespace() {
439        assert_eq!(parse_near_token("  5 NEAR  ").unwrap().as_near(), 5);
440    }
441
442    #[test]
443    fn test_near_token_parse_invalid_format() {
444        assert!(matches!(
445            parse_near_token("5 ETH"),
446            Err(ParseAmountError::InvalidFormat(_))
447        ));
448    }
449
450    #[test]
451    fn test_near_token_parse_invalid_number() {
452        assert!(matches!(
453            parse_near_token("abc NEAR"),
454            Err(ParseAmountError::InvalidNumber(_))
455        ));
456    }
457
458    #[test]
459    fn test_near_token_try_from_str() {
460        let token = "5 NEAR".into_near_token().unwrap();
461        assert_eq!(token.as_near(), 5);
462    }
463
464    // ========================================================================
465    // NearToken serde tests
466    // ========================================================================
467
468    #[test]
469    fn test_near_token_serde_roundtrip() {
470        let amount = NearToken::from_near(5);
471        let json = serde_json::to_string(&amount).unwrap();
472        // Upstream serializes as string (yoctoNEAR)
473        assert_eq!(json, format!("\"{}\"", amount.as_yoctonear()));
474
475        let parsed: NearToken = serde_json::from_str(&json).unwrap();
476        assert_eq!(amount, parsed);
477    }
478
479    #[test]
480    fn test_near_token_borsh_roundtrip() {
481        let amount = NearToken::from_near(10);
482        let bytes = borsh::to_vec(&amount).unwrap();
483        let parsed: NearToken = borsh::from_slice(&bytes).unwrap();
484        assert_eq!(amount, parsed);
485    }
486
487    // ========================================================================
488    // NearToken comparison tests
489    // ========================================================================
490
491    #[test]
492    fn test_near_token_ord() {
493        let small = NearToken::from_near(1);
494        let large = NearToken::from_near(10);
495        assert!(small < large);
496        assert!(large > small);
497        assert!(small <= small);
498        assert!(small >= small);
499    }
500
501    #[test]
502    fn test_near_token_eq() {
503        let a = NearToken::from_near(5);
504        let b = NearToken::from_millinear(5000);
505        assert_eq!(a, b);
506    }
507
508    #[test]
509    fn test_near_token_hash() {
510        use std::collections::HashSet;
511        let mut set = HashSet::new();
512        set.insert(NearToken::from_near(1));
513        set.insert(NearToken::from_near(2));
514        assert!(set.contains(&NearToken::from_near(1)));
515        assert!(!set.contains(&NearToken::from_near(3)));
516    }
517
518    // ========================================================================
519    // Gas tests
520    // ========================================================================
521
522    #[test]
523    fn test_gas_constructors() {
524        assert_eq!(Gas::from_gas(1000).as_gas(), 1000);
525        assert_eq!(Gas::from_tgas(30).as_gas(), 30 * GAS_PER_TGAS);
526        assert_eq!(Gas::from_ggas(5).as_gas(), 5 * GAS_PER_GGAS);
527    }
528
529    #[test]
530    fn test_gas_as_accessors() {
531        let gas = Gas::from_tgas(30);
532        assert_eq!(gas.as_tgas(), 30);
533        assert_eq!(gas.as_ggas(), 30_000);
534        assert_eq!(gas.as_gas(), 30 * GAS_PER_TGAS);
535    }
536
537    #[test]
538    fn test_gas_is_zero() {
539        assert!(Gas::from_gas(0).is_zero());
540        assert!(!Gas::from_ggas(1).is_zero());
541    }
542
543    #[test]
544    fn test_gas_checked_add() {
545        let a = Gas::from_tgas(10);
546        let b = Gas::from_tgas(20);
547        assert_eq!(a.checked_add(b).unwrap().as_tgas(), 30);
548
549        // Overflow
550        let max = Gas::from_gas(u64::MAX);
551        assert!(max.checked_add(Gas::from_gas(1)).is_none());
552    }
553
554    #[test]
555    fn test_gas_checked_sub() {
556        let a = Gas::from_tgas(30);
557        let b = Gas::from_tgas(10);
558        assert_eq!(a.checked_sub(b).unwrap().as_tgas(), 20);
559
560        // Underflow
561        assert!(b.checked_sub(a).is_none());
562    }
563
564    #[test]
565    fn test_gas_parse_tgas_variants() {
566        assert_eq!(parse_gas("30 Tgas").unwrap().as_tgas(), 30);
567        assert_eq!(parse_gas("30 tgas").unwrap().as_tgas(), 30);
568        assert_eq!(parse_gas("30 TGas").unwrap().as_tgas(), 30);
569    }
570
571    #[test]
572    fn test_gas_parse_ggas_variants() {
573        assert_eq!(parse_gas("5 Ggas").unwrap().as_ggas(), 5);
574        assert_eq!(parse_gas("5 ggas").unwrap().as_ggas(), 5);
575        assert_eq!(parse_gas("5 GGas").unwrap().as_ggas(), 5);
576    }
577
578    #[test]
579    fn test_gas_parse_invalid_format() {
580        assert!(matches!(
581            parse_gas("30 teragas"),
582            Err(ParseGasError::InvalidFormat(_))
583        ));
584    }
585
586    #[test]
587    fn test_gas_parse_invalid_number() {
588        assert!(matches!(
589            parse_gas("abc Tgas"),
590            Err(ParseGasError::InvalidNumber(_))
591        ));
592    }
593
594    #[test]
595    fn test_gas_try_from_str() {
596        let gas = "30 Tgas".into_gas().unwrap();
597        assert_eq!(gas.as_tgas(), 30);
598    }
599
600    #[test]
601    fn test_gas_serde_roundtrip() {
602        let gas = Gas::from_tgas(30);
603        let json = serde_json::to_string(&gas).unwrap();
604        let parsed: Gas = serde_json::from_str(&json).unwrap();
605        assert_eq!(gas, parsed);
606    }
607
608    #[test]
609    fn test_gas_borsh_roundtrip() {
610        let gas = Gas::from_tgas(30);
611        let bytes = borsh::to_vec(&gas).unwrap();
612        let parsed: Gas = borsh::from_slice(&bytes).unwrap();
613        assert_eq!(gas, parsed);
614    }
615
616    #[test]
617    fn test_gas_ord() {
618        let small = Gas::from_tgas(10);
619        let large = Gas::from_tgas(100);
620        assert!(small < large);
621    }
622
623    // ========================================================================
624    // IntoNearToken tests
625    // ========================================================================
626
627    #[test]
628    fn test_into_near_token_from_near_token() {
629        let token = NearToken::from_near(5);
630        assert_eq!(token.into_near_token().unwrap(), NearToken::from_near(5));
631    }
632
633    #[test]
634    fn test_into_near_token_from_str() {
635        assert_eq!("5 NEAR".into_near_token().unwrap(), NearToken::from_near(5));
636    }
637
638    #[test]
639    fn test_into_near_token_from_string() {
640        let s = String::from("5 NEAR");
641        assert_eq!(s.into_near_token().unwrap(), NearToken::from_near(5));
642    }
643
644    #[test]
645    fn test_into_near_token_from_string_ref() {
646        let s = String::from("5 NEAR");
647        assert_eq!((&s).into_near_token().unwrap(), NearToken::from_near(5));
648    }
649
650    // ========================================================================
651    // IntoGas tests
652    // ========================================================================
653
654    #[test]
655    fn test_into_gas_from_gas() {
656        let gas = Gas::from_tgas(30);
657        assert_eq!(gas.into_gas().unwrap(), Gas::from_tgas(30));
658    }
659
660    #[test]
661    fn test_into_gas_from_str() {
662        assert_eq!("30 Tgas".into_gas().unwrap(), Gas::from_tgas(30));
663    }
664
665    #[test]
666    fn test_into_gas_from_string() {
667        let s = String::from("30 Tgas");
668        assert_eq!(s.into_gas().unwrap(), Gas::from_tgas(30));
669    }
670
671    #[test]
672    fn test_into_gas_from_string_ref() {
673        let s = String::from("30 Tgas");
674        assert_eq!((&s).into_gas().unwrap(), Gas::from_tgas(30));
675    }
676
677    // ========================================================================
678    // Edge case tests
679    // ========================================================================
680
681    #[test]
682    fn test_near_token_default() {
683        let default = NearToken::default();
684        assert_eq!(default, NearToken::from_yoctonear(0));
685    }
686
687    #[test]
688    fn test_gas_default_trait() {
689        let default = Gas::default();
690        assert_eq!(default, Gas::from_gas(0));
691    }
692
693    #[test]
694    fn test_near_token_debug() {
695        let token = NearToken::from_near(5);
696        let debug = format!("{:?}", token);
697        assert!(debug.contains("NearToken"));
698    }
699
700    #[test]
701    fn test_gas_debug() {
702        let gas = Gas::from_tgas(30);
703        let debug = format!("{:?}", gas);
704        assert!(debug.contains("NearGas"));
705    }
706}