diff --git a/crates/miden-protocol/src/asset/asset_amount.rs b/crates/miden-protocol/src/asset/asset_amount.rs new file mode 100644 index 0000000000..b2688d6540 --- /dev/null +++ b/crates/miden-protocol/src/asset/asset_amount.rs @@ -0,0 +1,158 @@ +use alloc::string::ToString; +use core::fmt; + +use super::super::errors::AssetError; +use super::super::utils::serde::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, +}; + +// ASSET AMOUNT +// ================================================================================================ + +/// A validated fungible asset amount. +/// +/// Wraps a `u64` that is guaranteed to be at most [`AssetAmount::MAX`]. This type is used in +/// [`FungibleAsset`](super::FungibleAsset) to ensure the amount is always valid. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssetAmount(u64); + +impl AssetAmount { + /// The maximum value an asset amount can represent. + /// + /// Equal to 2^63 - 2^31. This was chosen so that the amount fits as both a positive and + /// negative value in a field element. + pub const MAX: Self = Self(2u64.pow(63) - 2u64.pow(31)); + + /// Returns a new `AssetAmount` if `amount` does not exceed [`Self::MAX`]. + /// + /// # Errors + /// + /// Returns an error if `amount` is greater than [`Self::MAX`]. + pub fn new(amount: u64) -> Result { + if amount > Self::MAX.0 { + return Err(AssetError::FungibleAssetAmountTooBig(amount)); + } + Ok(Self(amount)) + } + + /// Returns the inner `u64` value. + pub const fn as_u64(&self) -> u64 { + self.0 + } +} + +// CONVERSIONS +// ================================================================================================ + +impl From for AssetAmount { + fn from(value: u8) -> Self { + Self(value as u64) + } +} + +impl From for AssetAmount { + fn from(value: u16) -> Self { + Self(value as u64) + } +} + +impl From for AssetAmount { + fn from(value: u32) -> Self { + Self(value as u64) + } +} + +impl TryFrom for AssetAmount { + type Error = AssetError; + + fn try_from(value: u64) -> Result { + Self::new(value) + } +} + +impl From for u64 { + fn from(amount: AssetAmount) -> Self { + amount.0 + } +} + +// DISPLAY +// ================================================================================================ + +impl fmt::Display for AssetAmount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AssetAmount { + fn write_into(&self, target: &mut W) { + target.write(self.0); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + } +} + +impl Deserializable for AssetAmount { + fn read_from(source: &mut R) -> Result { + let amount: u64 = source.read()?; + Self::new(amount).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_amounts() { + assert_eq!(AssetAmount::new(0).unwrap().as_u64(), 0); + assert_eq!(AssetAmount::new(1000).unwrap().as_u64(), 1000); + assert_eq!(AssetAmount::new(AssetAmount::MAX.as_u64()).unwrap(), AssetAmount::MAX); + } + + #[test] + fn exceeds_max() { + assert!(AssetAmount::new(AssetAmount::MAX.as_u64() + 1).is_err()); + assert!(AssetAmount::new(u64::MAX).is_err()); + } + + #[test] + fn from_small_types() { + let a: AssetAmount = 42u8.into(); + assert_eq!(a.as_u64(), 42); + + let b: AssetAmount = 1000u16.into(); + assert_eq!(b.as_u64(), 1000); + + let c: AssetAmount = 100_000u32.into(); + assert_eq!(c.as_u64(), 100_000); + } + + #[test] + fn try_from_u64() { + assert!(AssetAmount::try_from(0u64).is_ok()); + assert!(AssetAmount::try_from(AssetAmount::MAX.as_u64()).is_ok()); + assert!(AssetAmount::try_from(AssetAmount::MAX.as_u64() + 1).is_err()); + } + + #[test] + fn display() { + assert_eq!(AssetAmount::new(12345).unwrap().to_string(), "12345"); + } + + #[test] + fn into_u64() { + let amount = AssetAmount::new(500).unwrap(); + let raw: u64 = amount.into(); + assert_eq!(raw, 500); + } +} diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index 58b5754663..5c87dd0a3b 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -1,6 +1,7 @@ use alloc::string::ToString; use core::fmt; +use super::AssetAmount; use super::vault::AssetVaultKey; use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word}; use crate::Felt; @@ -26,7 +27,7 @@ use crate::utils::serde::{ #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct FungibleAsset { faucet_id: AccountId, - amount: u64, + amount: AssetAmount, callbacks: AssetCallbackFlag, } @@ -37,7 +38,7 @@ impl FungibleAsset { /// /// This number was chosen so that it can be represented as a positive and negative number in a /// field element. See `account_delta.masm` for more details on how this number was chosen. - pub const MAX_AMOUNT: u64 = 2u64.pow(63) - 2u64.pow(31); + pub const MAX_AMOUNT: u64 = AssetAmount::MAX.as_u64(); /// The serialized size of a [`FungibleAsset`] in bytes. /// @@ -61,9 +62,7 @@ impl FungibleAsset { return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id)); } - if amount > Self::MAX_AMOUNT { - return Err(AssetError::FungibleAssetAmountTooBig(amount)); - } + let amount = AssetAmount::new(amount)?; Ok(Self { faucet_id, @@ -127,7 +126,7 @@ impl FungibleAsset { /// Returns the amount of this asset. pub fn amount(&self) -> u64 { - self.amount + self.amount.as_u64() } /// Returns true if this and the other asset were issued from the same faucet. @@ -154,7 +153,7 @@ impl FungibleAsset { /// Returns the asset's value encoded to a [`Word`]. pub fn to_value_word(&self) -> Word { Word::new([ - Felt::try_from(self.amount) + Felt::try_from(self.amount.as_u64()) .expect("fungible asset should only allow amounts that fit into a felt"), Felt::ZERO, Felt::ZERO, @@ -180,13 +179,12 @@ impl FungibleAsset { }); } - let amount = self + let raw = self .amount - .checked_add(other.amount) + .as_u64() + .checked_add(other.amount.as_u64()) .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64"); - if amount > Self::MAX_AMOUNT { - return Err(AssetError::FungibleAssetAmountTooBig(amount)); - } + let amount = AssetAmount::new(raw)?; Ok(Self { faucet_id: self.faucet_id, @@ -210,12 +208,14 @@ impl FungibleAsset { }); } - let amount = self.amount.checked_sub(other.amount).ok_or( + let raw = self.amount.as_u64().checked_sub(other.amount.as_u64()).ok_or( AssetError::FungibleAssetAmountNotSufficient { - minuend: self.amount, - subtrahend: other.amount, + minuend: self.amount.as_u64(), + subtrahend: other.amount.as_u64(), }, )?; + let amount = AssetAmount::new(raw) + .expect("subtraction of valid amounts should produce a valid amount"); Ok(FungibleAsset { faucet_id: self.faucet_id, @@ -246,13 +246,13 @@ impl Serializable for FungibleAsset { // All assets should serialize their faucet ID at the first position to allow them to be // distinguishable during deserialization. target.write(self.faucet_id); - target.write(self.amount); + target.write(self.amount.as_u64()); target.write(self.callbacks); } fn get_size_hint(&self) -> usize { self.faucet_id.get_size_hint() - + self.amount.get_size_hint() + + self.amount.as_u64().get_size_hint() + self.callbacks.get_size_hint() } } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 4bdec21c38..3f6cde41f1 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -10,6 +10,9 @@ use super::utils::serde::{ use super::{Felt, Word}; use crate::account::AccountId; +mod asset_amount; +pub use asset_amount::AssetAmount; + mod fungible; pub use fungible::FungibleAsset;