1use 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#[derive(
20 Debug, PartialEq, Clone, Copy, Default, Read, Write, NumBytes, Deserialize,
21)]
22#[eosio_bytes_root_path = "::eosio_bytes"]
23pub struct Asset {
24 pub amount: i64,
26 pub symbol: Symbol,
28}
29
30impl Asset {
31 #[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#[derive(Debug, PartialEq, Clone, Copy)]
62pub enum ParseAssetError {
63 BadChar(char),
65 BadPrecision,
67 SymbolIsEmpty,
69 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 let s = s.trim();
91 let mut chars = s.chars();
92 let mut index = 0_usize;
93 let mut precision: Option<u64> = None;
94 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 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#[derive(Debug, Clone, Copy)]
187pub enum AssetOpError {
188 Overflow,
190 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#[derive(Debug, Clone, Copy)]
209pub enum AssetDivOpError {
210 Overflow,
212 DifferentSymbols,
214 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}