eosio_core/
asset.rs

1//! TODO docs
2use crate::{
3    CheckedAdd, CheckedDiv, CheckedMul, CheckedRem, CheckedSub,
4    ParseSymbolError, Symbol,
5};
6use eosio_bytes::{NumBytes, Read, Write};
7use eosio_numstr::symbol_from_chars;
8use serde::{Deserialize, Serialize, Serializer};
9use std::convert::TryFrom;
10use std::error::Error;
11use std::fmt;
12use std::ops::{
13    Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, RemAssign, Sub,
14    SubAssign,
15};
16use std::str::FromStr;
17
18/// TODO docs
19#[derive(
20    Debug, PartialEq, Clone, Copy, Default, Read, Write, NumBytes, Deserialize,
21)]
22#[eosio_bytes_root_path = "::eosio_bytes"]
23pub struct Asset {
24    /// TODO docs
25    pub amount: i64,
26    /// TODO docs
27    pub symbol: Symbol,
28}
29
30impl Asset {
31    /// TODO docs
32    #[inline]
33    pub fn is_valid(&self) -> bool {
34        self.symbol.is_valid()
35    }
36}
37
38impl fmt::Display for Asset {
39    #[inline]
40    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41        let precision = self.symbol.precision();
42        let symbol_code = self.symbol.code();
43        if precision == 0 {
44            write!(f, "{} {}", self.amount, symbol_code)
45        } else {
46            let precision = usize::from(precision);
47            let formatted = format!(
48                "{:0precision$}",
49                self.amount,
50                precision = precision + if self.amount < 0 { 2 } else { 1 }
51            );
52            let index = formatted.len() - precision;
53            let whole = formatted.get(..index).unwrap_or_else(|| "");
54            let fraction = formatted.get(index..).unwrap_or_else(|| "");
55            write!(f, "{}.{} {}", whole, fraction, symbol_code)
56        }
57    }
58}
59
60/// TODO docs
61#[derive(Debug, PartialEq, Clone, Copy)]
62pub enum ParseAssetError {
63    /// TODO docs
64    BadChar(char),
65    /// TODO docs
66    BadPrecision,
67    /// TODO docs
68    SymbolIsEmpty,
69    /// TODO docs
70    SymbolTooLong,
71}
72
73impl From<ParseSymbolError> for ParseAssetError {
74    #[inline]
75    fn from(value: ParseSymbolError) -> Self {
76        match value {
77            ParseSymbolError::IsEmpty => ParseAssetError::SymbolIsEmpty,
78            ParseSymbolError::TooLong => ParseAssetError::SymbolTooLong,
79            ParseSymbolError::BadChar(c) => ParseAssetError::BadChar(c),
80            ParseSymbolError::BadPrecision => ParseAssetError::BadPrecision,
81        }
82    }
83}
84
85impl FromStr for Asset {
86    type Err = ParseAssetError;
87    #[inline]
88    fn from_str(s: &str) -> Result<Self, Self::Err> {
89        // TODO: refactor ugly code below
90        let s = s.trim();
91        let mut chars = s.chars();
92        let mut index = 0_usize;
93        let mut precision: Option<u64> = None;
94        // Find numbers
95        loop {
96            let c = match chars.next() {
97                Some(c) => c,
98                None => return Err(ParseAssetError::SymbolIsEmpty),
99            };
100            if index == 0 {
101                if '0' <= c && c <= '9' || c == '-' || c == '+' {
102                    index += 1;
103                    continue;
104                } else {
105                    return Err(ParseAssetError::BadChar(c));
106                }
107            }
108
109            index += 1;
110            if '0' <= c && c <= '9' {
111                if let Some(p) = precision {
112                    precision = Some(p + 1);
113                }
114            } else if c == ' ' {
115                match precision {
116                    Some(0) => return Err(ParseAssetError::BadPrecision),
117                    _ => break,
118                }
119            } else if c == '.' {
120                precision = Some(0);
121            } else {
122                return Err(ParseAssetError::BadChar(c));
123            }
124        }
125
126        let precision = u8::try_from(precision.unwrap_or_default())
127            .map_err(|_| ParseAssetError::BadPrecision)?;
128        let symbol = symbol_from_chars(precision, chars)
129            .map_err(ParseAssetError::from)?;
130
131        let end_index = if precision == 0 {
132            index
133        } else {
134            index - (precision as usize) - 1
135        } as usize;
136        // TODO: clean up code/unwraps below
137        let amount = s.get(0..end_index - 1).unwrap();
138        if precision == 0 {
139            let amount =
140                amount.parse::<i64>().expect("error parsing asset amount");
141            Ok(Self {
142                amount,
143                symbol: symbol.into(),
144            })
145        } else {
146            let fraction = s.get(end_index..(index - 1) as usize).unwrap();
147            let amount = format!("{}{}", amount, fraction)
148                .parse::<i64>()
149                .expect("error parsing asset amount");
150            Ok(Self {
151                amount,
152                symbol: symbol.into(),
153            })
154        }
155    }
156}
157
158impl TryFrom<&str> for Asset {
159    type Error = ParseAssetError;
160    #[inline]
161    fn try_from(value: &str) -> Result<Self, Self::Error> {
162        Self::from_str(value)
163    }
164}
165
166impl TryFrom<String> for Asset {
167    type Error = ParseAssetError;
168    #[inline]
169    fn try_from(value: String) -> Result<Self, Self::Error> {
170        Self::try_from(value.as_str())
171    }
172}
173
174impl Serialize for Asset {
175    #[inline]
176    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177    where
178        S: Serializer,
179    {
180        let s = self.to_string();
181        serializer.serialize_str(s.as_str())
182    }
183}
184
185/// TODO docs
186#[derive(Debug, Clone, Copy)]
187pub enum AssetOpError {
188    /// TODO docs
189    Overflow,
190    /// TODO docs
191    DifferentSymbols,
192}
193
194impl fmt::Display for AssetOpError {
195    #[inline]
196    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197        let msg = match *self {
198            AssetOpError::Overflow => "integer overflow",
199            AssetOpError::DifferentSymbols => "assets have different symbols",
200        };
201        write!(f, "{}", msg)
202    }
203}
204
205impl Error for AssetOpError {}
206
207/// TODO docs
208#[derive(Debug, Clone, Copy)]
209pub enum AssetDivOpError {
210    /// TODO docs
211    Overflow,
212    /// TODO docs
213    DifferentSymbols,
214    /// TODO docs
215    DivideByZero,
216}
217
218impl fmt::Display for AssetDivOpError {
219    #[inline]
220    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221        let msg = match *self {
222            AssetDivOpError::Overflow => "integer overflow",
223            AssetDivOpError::DifferentSymbols => {
224                "assets have different symbols"
225            }
226            AssetDivOpError::DivideByZero => "divide by zero",
227        };
228        write!(f, "{}", msg)
229    }
230}
231
232impl Error for AssetDivOpError {}
233
234macro_rules! impl_op {
235    ($($checked_trait:ident, $checked_error:ident, $checked_fn:ident, $op_trait:ident, $op_fn:ident, $assign_trait:ident, $assign_fn:ident)*) => ($(
236        impl $checked_trait<i64> for Asset {
237            type Output = Option<Self>;
238            #[inline]
239            fn $checked_fn(self, other: i64) -> Self::Output {
240                self.amount.$checked_fn(other).map(|amount| Self {
241                    amount,
242                    symbol: self.symbol,
243                })
244            }
245        }
246
247        impl $checked_trait<u64> for Asset {
248            type Output = Option<Self>;
249            #[inline]
250            fn $checked_fn(self, other: u64) -> Self::Output {
251                u64::try_from(other).ok().and_then(|other| self.$checked_fn(other))
252            }
253        }
254
255        impl $checked_trait<u128> for Asset {
256            type Output = Option<Self>;
257            #[inline]
258            fn $checked_fn(self, other: u128) -> Self::Output {
259                u64::try_from(other).ok().and_then(|other| self.$checked_fn(other))
260            }
261        }
262
263        impl $checked_trait<i128> for Asset {
264            type Output = Option<Self>;
265            #[inline]
266            fn $checked_fn(self, other: i128) -> Self::Output {
267                u64::try_from(other).ok().and_then(|other| self.$checked_fn(other))
268            }
269        }
270
271        impl $checked_trait<isize> for Asset {
272            type Output = Option<Self>;
273            #[inline]
274            fn $checked_fn(self, other: isize) -> Self::Output {
275                u64::try_from(other).ok().and_then(|other| self.$checked_fn(other))
276            }
277        }
278
279        impl $checked_trait<usize> for Asset {
280            type Output = Option<Self>;
281            #[inline]
282            fn $checked_fn(self, other: usize) -> Self::Output {
283                u64::try_from(other).ok().and_then(|other| self.$checked_fn(other))
284            }
285        }
286
287        impl $checked_trait for Asset {
288            type Output = Result<Self, $checked_error>;
289            #[inline]
290            fn $checked_fn(self, other: Self) -> Self::Output {
291                if self.symbol == other.symbol {
292                    self.$checked_fn(other.amount)
293                        .ok_or_else(|| $checked_error::Overflow)
294                } else {
295                    Err($checked_error::DifferentSymbols)
296                }
297            }
298        }
299
300        impl $op_trait for Asset {
301            type Output = Self;
302            #[inline]
303            fn $op_fn(self, rhs: Self) -> Self::Output {
304                match self.$checked_fn(rhs) {
305                    Ok(output) => output,
306                    Err(error) => panic!(
307                        "can't perform operation on asset, {}", error
308                    ),
309                }
310            }
311        }
312
313        impl $op_trait<i64> for Asset {
314            type Output = Self;
315            #[inline]
316            fn $op_fn(self, rhs: i64) -> Self::Output {
317                match self.$checked_fn(rhs) {
318                    Some(output) => output,
319                    None => panic!(
320                        "can't perform operation on asset, result would overflow"
321                    ),
322                }
323            }
324        }
325
326        impl $op_trait<Asset> for i64 {
327            type Output = Asset;
328            #[inline]
329            fn $op_fn(self, rhs: Asset) -> Self::Output {
330                rhs.$op_fn(self)
331            }
332        }
333
334        impl $assign_trait for Asset {
335            #[inline]
336            fn $assign_fn(&mut self, rhs: Self) {
337                *self = self.$op_fn(rhs);
338            }
339        }
340
341        impl $assign_trait<i64> for Asset {
342            #[inline]
343            fn $assign_fn(&mut self, rhs: i64) {
344                *self = self.$op_fn(rhs);
345            }
346        }
347    )*)
348}
349
350impl_op! {
351    CheckedAdd, AssetOpError, checked_add, Add, add, AddAssign, add_assign
352    CheckedSub, AssetOpError, checked_sub, Sub, sub, SubAssign, sub_assign
353    CheckedMul, AssetOpError, checked_mul, Mul, mul, MulAssign, mul_assign
354    CheckedDiv, AssetDivOpError, checked_div, Div, div, DivAssign, div_assign
355    CheckedRem, AssetOpError, checked_rem, Rem, rem, RemAssign, rem_assign
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use eosio_numstr_macros::{n, s};
362
363    macro_rules! test_to_string {
364        ($($name:ident, $amount:expr, $symbol:expr, $expected:expr)*) => ($(
365            #[test]
366            fn $name() {
367                let asset = Asset {
368                    amount: $amount,
369                    symbol: $symbol.into(),
370                };
371                assert_eq!(asset.to_string(), $expected);
372            }
373        )*)
374    }
375
376    test_to_string! {
377        to_string, 1_0000, s!(4, EOS), "1.0000 EOS"
378        to_string_signed, -1_0000, s!(4, EOS), "-1.0000 EOS"
379        to_string_fraction, 1_0001, s!(4, EOS), "1.0001 EOS"
380        to_string_zero_precision, 10_001, s!(0, EOS), "10001 EOS"
381        to_string_zero_precision_unsigned, -10_001, s!(0, EOS), "-10001 EOS"
382        to_string_max_number, std::i64::MAX, s!(4, EOS), "922337203685477.5807 EOS"
383        to_string_min_number, std::i64::MIN, s!(4, EOS), "-922337203685477.5808 EOS"
384        to_string_very_small_number, 1, s!(255, TST), "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 TST"
385        to_string_very_small_number_neg, -1, s!(255, TST), "-0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 TST"
386    }
387
388    macro_rules! test_from_str_ok {
389        ($($name:ident, $input:expr, $expected_amount:expr, $expected_symbol:expr)*) => ($(
390            #[test]
391            fn $name() {
392                let ok = Ok(Asset {
393                    amount: $expected_amount,
394                    symbol: $expected_symbol.into(),
395                });
396                assert_eq!(Asset::from_str($input), ok);
397                assert_eq!(Asset::try_from($input), ok);
398            }
399        )*)
400    }
401
402    test_from_str_ok! {
403        from_str_ok_basic, "1.0000 EOS", 1_0000, s!(4, EOS)
404        from_str_ok_zero_precision, "1 TST", 1, s!(0, TST)
405        from_str_ok_long, "1234567890.12345 TMP", 1234567890_12345, s!(5, TMP)
406        from_str_ok_signed_neg, "-1.0000 TLOS", -1_0000, s!(4, TLOS)
407        from_str_ok_signed_zero_precision, "-1 SYS", -1, s!(0, SYS)
408        from_str_ok_signed_long, "-1234567890.12345 TGFT", -1234567890_12345, s!(5, TGFT)
409        from_str_ok_pos_sign, "+1 TST", 1, s!(0, TST)
410        from_str_ok_fraction, "0.0001 EOS", 1, s!(4, EOS)
411        from_str_ok_zero, "0.0000 EOS", 0, s!(4, EOS)
412        from_str_whitespace_around, "            1.0000 EOS   ", 1_0000, s!(4, EOS)
413        from_str_zero_padded, "0001.0000 EOS", 1_0000, s!(4, EOS)
414        from_str_very_small_num, "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 TST", 1, s!(255, TST)
415        from_str_very_small_num_neg, "-0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 TST", -1, s!(255, TST)
416    }
417
418    macro_rules! test_from_str_err {
419        ($($name:ident, $input:expr, $expected:expr)*) => ($(
420            #[test]
421            fn $name() {
422                let err = Err($expected);
423                assert_eq!(Asset::from_str($input), err);
424                assert_eq!(Asset::try_from($input), err);
425            }
426        )*)
427    }
428
429    test_from_str_err! {
430        from_str_bad_char1, "tst", ParseAssetError::BadChar('t')
431        from_str_multi_spaces, "1.0000  EOS", ParseAssetError::BadChar(' ')
432        from_str_lowercase_symbol, "1.0000 eos", ParseAssetError::BadChar('e')
433        from_str_no_space, "1EOS", ParseAssetError::BadChar('E')
434        from_str_no_symbol1, "1.2345 ", ParseAssetError::SymbolIsEmpty
435        from_str_no_symbol2, "1", ParseAssetError::SymbolIsEmpty
436        from_str_bad_char2, "1.a", ParseAssetError::BadChar('a')
437        from_str_bad_precision, "1. EOS", ParseAssetError::BadPrecision
438    }
439
440    #[test]
441    fn test_ops() {
442        let mut asset = Asset {
443            amount: 10_0000,
444            symbol: s!(4, EOS).into(),
445        };
446        asset += 1;
447        assert_eq!(asset.amount, 10_0001);
448        asset -= 1;
449        assert_eq!(asset.amount, 10_0000);
450        asset /= 10;
451        assert_eq!(asset.amount, 1_0000);
452    }
453}