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