1pub use near_gas::NearGas as Gas;
5pub use near_token::NearToken;
6
7use crate::error::{ParseAmountError, ParseGasError};
8
9const YOCTO_PER_NEAR: u128 = 1_000_000_000_000_000_000_000_000;
15const YOCTO_PER_MILLINEAR: u128 = 1_000_000_000_000_000_000_000;
17
18const GAS_PER_TGAS: u64 = 1_000_000_000_000;
20const GAS_PER_GGAS: u64 = 1_000_000_000;
22
23pub trait IntoNearToken {
48 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
76pub trait IntoGas {
101 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
129fn 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
182pub fn parse_near_token(s: &str) -> Result<NearToken, ParseAmountError> {
192 let s = s.trim();
193
194 if let Some(value) = s.strip_suffix(" NEAR").or_else(|| s.strip_suffix(" near")) {
196 return parse_near_decimal(value.trim());
197 }
198
199 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 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 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
234pub fn parse_gas(s: &str) -> Result<Gas, ParseGasError> {
241 let s = s.trim();
242
243 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 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 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 #[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 #[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); assert_eq!(NearToken::from_millinear(1500).as_near(), 1); }
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 #[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 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 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 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 assert_eq!(b.saturating_sub(a), NearToken::from_yoctonear(0));
398 }
399
400 #[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 #[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 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 #[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 #[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 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 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 #[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 #[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 #[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}