Skip to main content

miden_protocol/asset/
asset_amount.rs

1use alloc::string::ToString;
2use core::fmt;
3use core::ops::{Add, Sub};
4
5use super::super::errors::AssetError;
6use super::super::utils::serde::{
7    ByteReader,
8    ByteWriter,
9    Deserializable,
10    DeserializationError,
11    Serializable,
12};
13use crate::Felt;
14
15// ASSET AMOUNT
16// ================================================================================================
17
18/// A validated fungible asset amount.
19///
20/// Wraps a `u64` that is guaranteed to be at most [`AssetAmount::MAX`]. This type is used in
21/// [`FungibleAsset`](super::FungibleAsset) to ensure the amount is always valid.
22#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct AssetAmount(u64);
24
25impl AssetAmount {
26    /// The maximum value an asset amount can represent.
27    ///
28    /// Equal to 2^63 - 2^31. This was chosen so that the amount fits as both a positive and
29    /// negative value in a field element.
30    pub const MAX: Self = Self(2u64.pow(63) - 2u64.pow(31));
31
32    /// The zero amount.
33    pub const ZERO: Self = Self(0);
34
35    /// Returns a new `AssetAmount` if `amount` does not exceed [`Self::MAX`].
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if `amount` is greater than [`Self::MAX`].
40    pub fn new(amount: u64) -> Result<Self, AssetError> {
41        if amount > Self::MAX.0 {
42            return Err(AssetError::FungibleAssetAmountTooBig(amount));
43        }
44        Ok(Self(amount))
45    }
46
47    /// Returns the underlying `u64` value.
48    pub const fn as_u64(&self) -> u64 {
49        self.0
50    }
51
52    /// Returns the underlying value as an `i64`.
53    ///
54    /// SAFETY: this cast never truncates or wraps because [`Self::MAX`] (`2^63 - 2^31`) is
55    /// strictly less than [`i64::MAX`] (`2^63 - 1`), so every valid `AssetAmount` fits in a
56    /// non-negative `i64`.
57    pub const fn as_i64(&self) -> i64 {
58        self.0 as i64
59    }
60}
61
62impl Add for AssetAmount {
63    type Output = Result<Self, AssetError>;
64
65    fn add(self, other: Self) -> Self::Output {
66        let raw = self.0.checked_add(other.0).expect("even MAX + MAX should not overflow u64");
67        Self::new(raw)
68    }
69}
70
71impl Sub for AssetAmount {
72    type Output = Result<Self, AssetError>;
73
74    fn sub(self, other: Self) -> Self::Output {
75        let raw =
76            self.0
77                .checked_sub(other.0)
78                .ok_or(AssetError::FungibleAssetAmountNotSufficient {
79                    minuend: self.0,
80                    subtrahend: other.0,
81                })?;
82        Ok(Self(raw))
83    }
84}
85
86// CONVERSIONS
87// ================================================================================================
88
89impl From<u8> for AssetAmount {
90    fn from(value: u8) -> Self {
91        Self(value as u64)
92    }
93}
94
95impl From<u16> for AssetAmount {
96    fn from(value: u16) -> Self {
97        Self(value as u64)
98    }
99}
100
101impl From<u32> for AssetAmount {
102    fn from(value: u32) -> Self {
103        Self(value as u64)
104    }
105}
106
107impl TryFrom<u64> for AssetAmount {
108    type Error = AssetError;
109
110    fn try_from(value: u64) -> Result<Self, Self::Error> {
111        Self::new(value)
112    }
113}
114
115impl TryFrom<Felt> for AssetAmount {
116    type Error = AssetError;
117
118    fn try_from(value: Felt) -> Result<Self, Self::Error> {
119        Self::new(value.as_canonical_u64())
120    }
121}
122
123impl From<AssetAmount> for u64 {
124    fn from(amount: AssetAmount) -> Self {
125        amount.0
126    }
127}
128
129impl From<AssetAmount> for Felt {
130    fn from(amount: AssetAmount) -> Self {
131        Felt::try_from(amount.0).expect("asset amount should guarantee felt validity")
132    }
133}
134
135// DISPLAY
136// ================================================================================================
137
138impl fmt::Display for AssetAmount {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "{}", self.0)
141    }
142}
143
144// SERIALIZATION
145// ================================================================================================
146
147impl Serializable for AssetAmount {
148    fn write_into<W: ByteWriter>(&self, target: &mut W) {
149        target.write(self.0);
150    }
151
152    fn get_size_hint(&self) -> usize {
153        self.0.get_size_hint()
154    }
155}
156
157impl Deserializable for AssetAmount {
158    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
159        let amount: u64 = source.read()?;
160        Self::new(amount).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
161    }
162}
163
164// TESTS
165// ================================================================================================
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn valid_amounts() {
173        let val: u64 = AssetAmount::new(0).unwrap().into();
174        assert_eq!(val, 0);
175        let val: u64 = AssetAmount::new(1000).unwrap().into();
176        assert_eq!(val, 1000);
177        let val: u64 = AssetAmount::new(AssetAmount::MAX.0).unwrap().into();
178        assert_eq!(val, AssetAmount::MAX.0);
179    }
180
181    #[test]
182    fn exceeds_max() {
183        assert!(AssetAmount::new(AssetAmount::MAX.0 + 1).is_err());
184        assert!(AssetAmount::new(u64::MAX).is_err());
185    }
186
187    #[test]
188    fn from_small_types() {
189        let a: AssetAmount = 42u8.into();
190        let val: u64 = a.into();
191        assert_eq!(val, 42);
192
193        let b: AssetAmount = 1000u16.into();
194        let val: u64 = b.into();
195        assert_eq!(val, 1000);
196
197        let c: AssetAmount = 100_000u32.into();
198        let val: u64 = c.into();
199        assert_eq!(val, 100_000);
200    }
201
202    #[test]
203    fn try_from_u64() {
204        assert!(AssetAmount::try_from(0u64).is_ok());
205        assert!(AssetAmount::try_from(AssetAmount::MAX.0).is_ok());
206        assert!(AssetAmount::try_from(AssetAmount::MAX.0 + 1).is_err());
207    }
208
209    #[test]
210    fn display() {
211        assert_eq!(AssetAmount::new(12345).unwrap().to_string(), "12345");
212    }
213
214    #[test]
215    fn into_u64() {
216        let amount = AssetAmount::new(500).unwrap();
217        let raw: u64 = amount.into();
218        assert_eq!(raw, 500);
219    }
220
221    #[test]
222    fn add_amounts() {
223        let a = AssetAmount::new(100).unwrap();
224        let b = AssetAmount::new(200).unwrap();
225        let val: u64 = (a + b).unwrap().into();
226        assert_eq!(val, 300);
227    }
228
229    #[test]
230    fn add_overflow() {
231        let max = AssetAmount::new(AssetAmount::MAX.0).unwrap();
232        let one = AssetAmount::new(1).unwrap();
233        assert!((max + one).is_err());
234    }
235
236    #[test]
237    fn sub_amounts() {
238        let a = AssetAmount::new(300).unwrap();
239        let b = AssetAmount::new(100).unwrap();
240        let val: u64 = (a - b).unwrap().into();
241        assert_eq!(val, 200);
242    }
243
244    #[test]
245    fn sub_underflow() {
246        let a = AssetAmount::new(50).unwrap();
247        let b = AssetAmount::new(100).unwrap();
248        assert!((a - b).is_err());
249    }
250}