Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions crates/miden-protocol/src/asset/asset_amount.rs
Original file line number Diff line number Diff line change
@@ -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<Self, AssetError> {
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<u8> for AssetAmount {
fn from(value: u8) -> Self {
Self(value as u64)
}
}

impl From<u16> for AssetAmount {
fn from(value: u16) -> Self {
Self(value as u64)
}
}

impl From<u32> for AssetAmount {
fn from(value: u32) -> Self {
Self(value as u64)
}
}

impl TryFrom<u64> for AssetAmount {
type Error = AssetError;

fn try_from(value: u64) -> Result<Self, Self::Error> {
Self::new(value)
}
}

impl From<AssetAmount> 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<W: ByteWriter>(&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<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
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);
}
}
34 changes: 17 additions & 17 deletions crates/miden-protocol/src/asset/fungible.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
}

Expand All @@ -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.
///
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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()
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/miden-protocol/src/asset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down