-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathamount.rs
243 lines (208 loc) · 8.1 KB
/
amount.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
use rust_decimal::Decimal;
use rusty_money::{define_currency_set, FormattableCurrency, Money, MoneyError};
use std::{
convert::TryInto,
fmt::{self, Display},
num::TryFromIntError,
str::FromStr,
};
use thiserror::Error;
pub use supported::*;
use zkabacus_crypto::{
CustomerBalance, Error as PaymentAmountError, MerchantBalance, PaymentAmount,
};
use crate::protocol::Party;
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Amount {
pub(crate) money: Money<'static, supported::Currency>,
}
impl FromStr for Amount {
type Err = MoneyError;
/// Parse an amount specified like "100.00 XTZ"
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((amount, currency)) = s.split_once(' ') {
let currency = supported::find(currency).ok_or(MoneyError::InvalidCurrency)?;
let money = Money::from_str(amount, currency)?;
if money.is_positive() {
Ok(Amount { money })
} else {
Err(MoneyError::InvalidAmount)
}
} else {
Err(MoneyError::InvalidAmount)
}
}
}
impl From<CustomerBalance> for Amount {
fn from(value: CustomerBalance) -> Self {
// This unwrap is safe because the `into_inner` function guarantees a u64
// with value < i64::Max
// Developer note: to extend to multiple currencies, we will have to do something more
// clever than hardcoding XTZ here.
Amount::from_minor_units_of_currency(value.into_inner().try_into().unwrap(), XTZ)
}
}
impl From<MerchantBalance> for Amount {
fn from(value: MerchantBalance) -> Self {
// This unwrap is safe because the `into_inner` function guarantees a u64
// with value < i64::Max
// Developer note: to extend to multiple currencies, we will have to do something more
// clever than hardcoding XTZ here.
Amount::from_minor_units_of_currency(value.into_inner().try_into().unwrap(), XTZ)
}
}
impl TryInto<PaymentAmount> for Amount {
type Error = AmountParseError;
fn try_into(self) -> Result<PaymentAmount, Self::Error> {
// Convert the payment amount appropriately
let minor_units: i64 = self
.try_into_minor_units()
.ok_or(AmountParseError::InvalidValue)?;
// Squash into PaymentAmount
Ok(if minor_units < 0 {
PaymentAmount::pay_customer(minor_units.abs() as u64)
} else {
PaymentAmount::pay_merchant(minor_units as u64)
}?)
}
}
macro_rules! try_into_balance {
($balance_type:ident, $party:ident) => {
impl TryInto<$balance_type> for Amount {
type Error = BalanceConversionError;
fn try_into(self) -> Result<$balance_type, Self::Error> {
$balance_type::try_new(
self.try_into_minor_units()
.ok_or_else(|| Self::Error::InvalidDeposit(Party::Customer))?
.try_into()?,
)
.map_err(|_| Self::Error::InvalidDeposit(Party::$party))
}
}
};
}
try_into_balance!(CustomerBalance, Customer);
try_into_balance!(MerchantBalance, Merchant);
#[derive(Debug, Error)]
pub enum BalanceConversionError {
#[error("Could not convert {0} deposit into a valid balance")]
InvalidDeposit(Party),
#[error(transparent)]
BalanceTooLarge(#[from] TryFromIntError),
}
impl Display for Amount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.money.amount().fmt(f)?;
write!(f, " ")?;
self.money.currency().fmt(f)
}
}
impl Amount {
/// Convert this [`Amount`] into a unitless signed amount of the smallest denomination of its
/// currency, or fail if it is not representable as such.
pub fn try_into_minor_units(&self) -> Option<i64> {
// The amount of money, as a `Decimal` in *major* units (e.g. 1 USD = 1.00)
let amount: &Decimal = self.money.amount();
// The number of decimal places used to represent *minor* units (e.g. for USD, this is 2)
let exponent: u32 = self.money.currency().exponent();
// The number of minor units equivalent to the amount, as a `Decimal` (multiply by 10^e)
let minor_units = amount.checked_mul(Decimal::from(10u32.checked_pow(exponent)?))?;
// If the amount of currency has a fractional amount of minor units, fail
if minor_units != minor_units.trunc() {
return None;
}
// Convert whole-numbered `Decimal` of minor units into an `i64`
let scale: u32 = minor_units.scale();
let mantissa: i64 = minor_units.mantissa().try_into().ok()?;
let minor_units: i64 = mantissa.checked_div(10i64.checked_pow(scale)?)?;
Some(minor_units)
}
/// Get the currency of this [`Amount`].
pub fn currency(&self) -> &'static supported::Currency {
self.money.currency()
}
/// Convert a unitless signed number into an [`Amount`] in the given currency equal to that
/// number of the smallest denomination of the currency.
///
/// For example, one cent is the smallest denomination of the USD, so this function would
/// interpret the number `1` as "0.01 USD", if the currency was USD.
pub fn from_minor_units_of_currency(
minor_units: i64,
currency: &'static supported::Currency,
) -> Self {
let minor_units: Decimal = minor_units.into();
let major_units = minor_units / Decimal::from(10u32.pow(currency.exponent()));
Self {
money: Money::from_decimal(major_units, currency),
}
}
}
#[derive(Debug, Error)]
pub enum AmountParseError {
#[error("Unknown currency: {0}")]
UnknownCurrency(String),
#[error("Invalid format for currency amount")]
InvalidFormat,
#[error("Payment amount invalid for currency or out of range for channel")]
InvalidValue,
#[error(transparent)]
InvalidPaymentAmount(#[from] PaymentAmountError),
}
// Define only the currencies supported by this application
define_currency_set!(
supported {
// Copied from the rusty_money crypto-currency definitions
XTZ: {
code: "XTZ",
exponent: 6,
locale: EnUs,
minor_units: 1_000_000,
name: "Tezos",
symbol: "XTZ",
symbol_first: false,
}
}
);
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_and_extract_tezos() {
let tezos_amount = Amount::from_str("12.34 XTZ").expect("failed to parse");
let minor_amount = tezos_amount
.try_into_minor_units()
.expect("failed to get minor amount");
assert_eq!(12_340_000, minor_amount);
}
#[test]
fn round_trip_minor_units_tezos() {
let microtez = Amount::from_minor_units_of_currency(1, XTZ);
assert_eq!(1, microtez.try_into_minor_units().unwrap());
}
fn try_parse_invalid_balance<T>(amount: &str)
where
Amount: TryInto<T>,
{
let bad_amount = Amount::from_str(amount);
assert!(bad_amount.is_err() || TryInto::<T>::try_into(bad_amount.unwrap()).is_err());
}
#[test]
fn test_balance_parsing() {
// Parsing fails with too many decimal places
let too_many_decimals_amount = Amount::from_str("1.55555555 XTZ").unwrap();
let customer_balance: Result<CustomerBalance, _> =
too_many_decimals_amount.clone().try_into();
assert!(customer_balance.is_err());
let merchant_balance: Result<MerchantBalance, _> = too_many_decimals_amount.try_into();
assert!(merchant_balance.is_err());
// Parsing fails on too-large numbers
try_parse_invalid_balance::<CustomerBalance>("9223372036854775810 XTZ");
try_parse_invalid_balance::<MerchantBalance>("9223372036854775810 XTZ");
// Parsing fails on negative numbers
try_parse_invalid_balance::<CustomerBalance>("-5 XTZ");
try_parse_invalid_balance::<MerchantBalance>("-5 XTZ");
// Parsing fails on zero
try_parse_invalid_balance::<CustomerBalance>("0 XTZ");
try_parse_invalid_balance::<MerchantBalance>("0 XTZ");
}
}