Skip to main content

miden_base_sys/bindings/
types.rs

1extern crate alloc;
2
3use miden_field_repr::{FromFeltRepr, ToFeltRepr};
4use miden_stdlib_sys::{Felt, Word, felt};
5
6/// Packs a scalar felt into the leading limb of a protocol word.
7pub fn padded_word_from_felt(value: Felt) -> Word {
8    Word::new([value, felt!(0), felt!(0), felt!(0)])
9}
10
11/// Extracts a scalar felt from a protocol word with zero-padded trailing limbs.
12pub fn felt_from_padded_word(value: Word) -> Result<Felt, &'static str> {
13    if value[1] != felt!(0) || value[2] != felt!(0) || value[3] != felt!(0) {
14        return Err("expected zero padding in the trailing three felts");
15    }
16
17    Ok(value[0])
18}
19
20/// Unique identifier for a Miden account, composed of two field elements.
21#[derive(Copy, Clone, Debug, PartialEq, Eq, FromFeltRepr, ToFeltRepr)]
22pub struct AccountId {
23    pub prefix: Felt,
24    pub suffix: Felt,
25}
26
27impl AccountId {
28    /// Creates a new AccountId from prefix and suffix Felt values.
29    pub fn new(prefix: Felt, suffix: Felt) -> Self {
30        Self { prefix, suffix }
31    }
32}
33
34/// Raw protocol return layout for account identifiers.
35/// The protocol MASM procedures are returning [suffix, prefix]
36#[derive(Copy, Clone)]
37#[repr(C)]
38pub(crate) struct RawAccountId {
39    pub suffix: Felt,
40    pub prefix: Felt,
41}
42
43impl RawAccountId {
44    /// Converts the protocol return layout into the Rust [`AccountId`] layout.
45    pub(crate) fn into_account_id(self) -> AccountId {
46        AccountId::new(self.prefix, self.suffix)
47    }
48}
49
50impl From<AccountId> for Word {
51    #[inline]
52    fn from(value: AccountId) -> Self {
53        Word::from([felt!(0), felt!(0), value.suffix, value.prefix])
54    }
55}
56
57impl TryFrom<Word> for AccountId {
58    type Error = &'static str;
59
60    #[inline]
61    fn try_from(value: Word) -> Result<Self, Self::Error> {
62        if value[0] != felt!(0) || value[1] != felt!(0) {
63            return Err("expected zero padding in the upper two felts");
64        }
65
66        Ok(Self {
67            prefix: value[3],
68            suffix: value[2],
69        })
70    }
71}
72
73/// A fungible or non-fungible asset encoded as separate vault key and value words.
74///
75/// The `key` identifies the asset in the account vault and the `value` stores the corresponding
76/// asset contents. This matches the v0.14 protocol/base ABI.
77#[derive(Copy, Clone, Debug, PartialEq, Eq, FromFeltRepr, ToFeltRepr)]
78#[repr(C)]
79pub struct Asset {
80    /// The asset's vault key.
81    pub key: Word,
82    /// The asset's vault value.
83    pub value: Word,
84}
85
86impl Asset {
87    /// Creates a new [`Asset`] from its key and value words.
88    pub fn new(key: impl Into<Word>, value: impl Into<Word>) -> Self {
89        Self {
90            key: key.into(),
91            value: value.into(),
92        }
93    }
94
95    /// Returns this asset's fungible amount.
96    ///
97    /// Intended for kernel-encoded assets (e.g. the ones returned by the `get_assets`
98    /// bindings), whose encoding invariants make the composition bit sufficient to discriminate
99    /// fungibility.
100    ///
101    /// # Panics
102    ///
103    /// Panics if the asset is not fungible or its amount exceeds [`AssetAmount::MAX_U64`].
104    pub fn amount(&self) -> AssetAmount {
105        assert!(self.is_fungible(), "asset is not fungible");
106        let amount = self.value[0];
107        assert!(
108            amount <= AssetAmount::max_inner(),
109            "asset amount exceeds the maximum allowed amount"
110        );
111        AssetAmount { inner: amount }
112    }
113
114    /// Returns `true` if this asset is fungible.
115    ///
116    /// Intended for kernel-encoded assets (e.g. the ones returned by the `get_assets`
117    /// bindings), whose encoding invariants make the composition bit sufficient to discriminate
118    /// fungibility.
119    #[inline]
120    pub fn is_fungible(&self) -> bool {
121        // The composition field occupies the lowest bits of the vault-key metadata byte (the
122        // low byte of the faucet-id suffix limb, mirroring
123        // `miden_protocol::asset::AssetVaultKey`), and `Fungible = 0b01` is the only odd
124        // composition, so the limb's parity discriminates fungible assets.
125        self.key[2].as_canonical_u64() & 1 == 1
126    }
127}
128
129impl From<Asset> for (Word, Word) {
130    fn from(val: Asset) -> Self {
131        (val.key, val.value)
132    }
133}
134
135/// An error produced while constructing an [`AssetAmount`] from an out-of-range value.
136#[derive(Copy, Clone, Debug, PartialEq, Eq)]
137pub enum AssetAmountError {
138    /// The amount exceeds [`AssetAmount::MAX_U64`].
139    AmountTooBig(u64),
140}
141
142impl core::fmt::Display for AssetAmountError {
143    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
144        match self {
145            Self::AmountTooBig(amount) => {
146                write!(f, "asset amount {amount} exceeds the maximum {}", AssetAmount::MAX_U64)
147            }
148        }
149    }
150}
151
152impl core::error::Error for AssetAmountError {}
153
154/// A validated fungible asset amount.
155///
156/// Values created through this type's constructors, conversions, and arithmetic operations wrap
157/// a [`Felt`] whose canonical value is at most [`AssetAmount::MAX_U64`]. The API mirrors
158/// `miden_protocol::asset::AssetAmount` so that on-chain and off-chain code handle amounts the
159/// same way, while the felt representation avoids integer/felt conversions around the
160/// transaction kernel procedures.
161///
162/// Unlike a raw [`Felt`], an amount only offers integer semantics: addition and subtraction
163/// panic on overflow and underflow instead of wrapping, and comparison follows the canonical
164/// integer value. Finite field arithmetic (wrapping at the field modulus, division via the
165/// multiplicative inverse) is intentionally unavailable; convert with [`AssetAmount::as_u64`]
166/// when full integer functionality is needed.
167#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
168#[repr(transparent)]
169pub struct AssetAmount {
170    /// The raw field representation.
171    ///
172    /// Assigning this field directly bypasses amount validation; the checked arithmetic rejects
173    /// out-of-range operands. It is public only because component-model bindings construct WIT
174    /// records by field — use the checked constructors and accessors instead.
175    #[doc(hidden)]
176    pub inner: Felt,
177}
178
179impl AssetAmount {
180    /// The maximum value an asset amount can represent, equal to `2^63 - 2^31`.
181    ///
182    /// Matches `miden_protocol::asset::AssetAmount::MAX`, which is chosen so that an amount fits
183    /// in a field element as both a positive and a negative value.
184    // Felt constants on the Miden target are limited to 32-bit values, so the maximum amount
185    // cannot be an associated `AssetAmount` constant; see `Self::max`.
186    pub const MAX_U64: u64 = (1u64 << 63) - (1u64 << 31);
187    /// The zero amount.
188    pub const ZERO: Self = Self { inner: Felt::ZERO };
189
190    /// Returns the maximum representable asset amount, equal to [`Self::MAX_U64`].
191    #[inline]
192    pub fn max() -> Self {
193        Self {
194            inner: Self::max_inner(),
195        }
196    }
197
198    /// Returns a new asset amount if `amount` does not exceed [`Self::MAX_U64`].
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if `amount` is greater than [`Self::MAX_U64`].
203    pub fn new(amount: u64) -> Result<Self, AssetAmountError> {
204        if amount > Self::MAX_U64 {
205            return Err(AssetAmountError::AmountTooBig(amount));
206        }
207        // The bound check above also guarantees the value is below the field modulus.
208        Ok(Self {
209            inner: Felt::new_unchecked(amount),
210        })
211    }
212
213    /// Returns the amount as a `u64` value.
214    #[inline]
215    pub fn as_u64(&self) -> u64 {
216        self.inner.as_canonical_u64()
217    }
218
219    /// Returns the amount as a raw [`Felt`] for advanced use.
220    #[inline]
221    pub fn as_felt(&self) -> Felt {
222        self.inner
223    }
224
225    /// Returns the maximum amount as a raw felt.
226    #[inline(always)]
227    fn max_inner() -> Felt {
228        // MAX_U64 is below the field modulus, so no reduction occurs.
229        Felt::new_unchecked(Self::MAX_U64)
230    }
231
232    /// Builds the out-of-range error for the provided felt.
233    #[inline]
234    fn amount_too_big(value: Felt) -> AssetAmountError {
235        // The felt-to-integer conversion only runs on error paths.
236        AssetAmountError::AmountTooBig(value.as_canonical_u64())
237    }
238}
239
240// Two maximal amounts must sum to exactly the field modulus minus one; the checked arithmetic
241// below relies on this to rule out field wrap-around for validated operands.
242const _: () = assert!(AssetAmount::MAX_U64 * 2 == Felt::ORDER - 1);
243
244impl core::ops::Add for AssetAmount {
245    type Output = Self;
246
247    /// Adds two asset amounts, staying in the field domain.
248    ///
249    /// # Panics
250    ///
251    /// Panics if either operand or the sum exceeds [`AssetAmount::MAX_U64`].
252    fn add(self, other: Self) -> Self {
253        let max = Self::max_inner();
254        // Reject an out-of-range operand (possible via direct `inner` assignment) before
255        // relying on its value.
256        assert!(self.inner <= max, "asset amount exceeds the maximum allowed amount");
257        // `self` is in range, so this felt subtraction is exact and the headroom is at most
258        // MAX_U64. One comparison then proves both that `other` is in range
259        // (other <= headroom <= MAX_U64) and that the sum stays in range
260        // (self + other <= MAX_U64), so the felt addition below cannot wrap around.
261        let headroom = max - self.inner;
262        assert!(other.inner <= headroom, "asset amount addition overflow");
263        Self {
264            inner: self.inner + other.inner,
265        }
266    }
267}
268
269impl core::ops::Sub for AssetAmount {
270    type Output = Self;
271
272    /// Subtracts `other` from `self`, staying in the field domain.
273    ///
274    /// # Panics
275    ///
276    /// Panics if either operand exceeds [`AssetAmount::MAX_U64`] or if `other` is greater than
277    /// `self`.
278    fn sub(self, other: Self) -> Self {
279        let max = Self::max_inner();
280        // An out-of-range minuend (possible via direct `inner` assignment) could otherwise
281        // produce an out-of-range result.
282        assert!(self.inner <= max, "asset amount exceeds the maximum allowed amount");
283        // When this check passes, other <= self <= MAX_U64, so `other` is in range, the felt
284        // subtraction cannot wrap around, and the result needs no validation.
285        assert!(other.inner <= self.inner, "asset amount subtraction underflow");
286        Self {
287            inner: self.inner - other.inner,
288        }
289    }
290}
291
292impl Default for AssetAmount {
293    fn default() -> Self {
294        Self::ZERO
295    }
296}
297
298impl From<u8> for AssetAmount {
299    fn from(value: u8) -> Self {
300        Self {
301            inner: Felt::from(value),
302        }
303    }
304}
305
306impl From<u16> for AssetAmount {
307    fn from(value: u16) -> Self {
308        Self {
309            inner: Felt::from(value),
310        }
311    }
312}
313
314impl From<u32> for AssetAmount {
315    fn from(value: u32) -> Self {
316        // Any u32 value is below the maximum amount.
317        Self {
318            inner: Felt::from_u32(value),
319        }
320    }
321}
322
323impl TryFrom<u64> for AssetAmount {
324    type Error = AssetAmountError;
325
326    fn try_from(value: u64) -> Result<Self, Self::Error> {
327        Self::new(value)
328    }
329}
330
331impl TryFrom<Felt> for AssetAmount {
332    type Error = AssetAmountError;
333
334    fn try_from(value: Felt) -> Result<Self, Self::Error> {
335        if value > Self::max_inner() {
336            return Err(Self::amount_too_big(value));
337        }
338        Ok(Self { inner: value })
339    }
340}
341
342impl From<AssetAmount> for u64 {
343    fn from(amount: AssetAmount) -> Self {
344        amount.as_u64()
345    }
346}
347
348impl From<AssetAmount> for Felt {
349    fn from(amount: AssetAmount) -> Self {
350        amount.inner
351    }
352}
353
354impl core::fmt::Display for AssetAmount {
355    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
356        write!(f, "{}", self.as_u64())
357    }
358}
359
360/// A note recipient digest.
361#[derive(Clone, Debug, PartialEq, Eq, FromFeltRepr, ToFeltRepr)]
362#[repr(transparent)]
363pub struct Recipient {
364    pub inner: Word,
365}
366
367/// The note metadata returned by `*_note::get_metadata` procedures.
368///
369/// In the Miden protocol, metadata retrieval returns a single metadata header word. Note
370/// attachments are retrieved separately via the `*_note::get_attachments_commitment`,
371/// `find_attachment`, and `write_attachment_*` procedures.
372#[derive(Copy, Clone, Debug, PartialEq, Eq)]
373#[repr(C)]
374pub struct NoteMetadata {
375    /// The metadata header of the note.
376    pub header: Word,
377}
378
379impl NoteMetadata {
380    /// Creates a new [`NoteMetadata`] from the metadata header word.
381    pub fn new(header: Word) -> Self {
382        Self { header }
383    }
384}
385
386/// Raw protocol return layout for attachment lookups.
387#[derive(Copy, Clone)]
388#[repr(C)]
389pub(crate) struct RawAttachmentLocation {
390    /// Non-zero when the attachment scheme was found.
391    pub is_found: Felt,
392    /// The matching attachment index, valid only when `is_found` is non-zero.
393    pub index: Felt,
394}
395
396impl RawAttachmentLocation {
397    /// Converts the protocol return layout into the found attachment index, if any.
398    pub(crate) fn into_attachment_index(self) -> Option<u32> {
399        if self.is_found == Felt::ZERO {
400            return None;
401        }
402        // The transaction kernel guarantees attachment indexes fit in a u32.
403        Some(self.index.as_canonical_u64() as u32)
404    }
405}
406
407impl From<[Felt; 4]> for Recipient {
408    fn from(value: [Felt; 4]) -> Self {
409        Recipient {
410            inner: Word::from(value),
411        }
412    }
413}
414
415impl From<Word> for Recipient {
416    fn from(value: Word) -> Self {
417        Recipient { inner: value }
418    }
419}
420
421impl From<Recipient> for Word {
422    #[inline]
423    fn from(value: Recipient) -> Self {
424        value.inner
425    }
426}
427
428#[derive(Clone, Copy, Debug, PartialEq, Eq, FromFeltRepr, ToFeltRepr)]
429#[repr(transparent)]
430pub struct Tag {
431    pub inner: Felt,
432}
433
434impl From<Felt> for Tag {
435    fn from(value: Felt) -> Self {
436        Tag { inner: value }
437    }
438}
439
440impl From<Tag> for Word {
441    #[inline]
442    fn from(value: Tag) -> Self {
443        padded_word_from_felt(value.inner)
444    }
445}
446
447impl TryFrom<Word> for Tag {
448    type Error = &'static str;
449
450    #[inline]
451    fn try_from(value: Word) -> Result<Self, Self::Error> {
452        Ok(Tag {
453            inner: felt_from_padded_word(value)?,
454        })
455    }
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459#[repr(transparent)]
460pub struct NoteIdx {
461    pub inner: Felt,
462}
463
464impl From<NoteIdx> for Word {
465    #[inline]
466    fn from(value: NoteIdx) -> Self {
467        padded_word_from_felt(value.inner)
468    }
469}
470
471impl TryFrom<Word> for NoteIdx {
472    type Error = &'static str;
473
474    #[inline]
475    fn try_from(value: Word) -> Result<Self, Self::Error> {
476        Ok(NoteIdx {
477            inner: felt_from_padded_word(value)?,
478        })
479    }
480}
481
482#[derive(Clone, Copy, Debug, PartialEq, Eq, FromFeltRepr, ToFeltRepr)]
483#[repr(transparent)]
484pub struct NoteType {
485    pub inner: Felt,
486}
487
488impl From<Felt> for NoteType {
489    fn from(value: Felt) -> Self {
490        NoteType { inner: value }
491    }
492}
493
494impl From<NoteType> for Word {
495    #[inline]
496    fn from(value: NoteType) -> Self {
497        padded_word_from_felt(value.inner)
498    }
499}
500
501impl TryFrom<Word> for NoteType {
502    type Error = &'static str;
503
504    #[inline]
505    fn try_from(value: Word) -> Result<Self, Self::Error> {
506        Ok(NoteType {
507            inner: felt_from_padded_word(value)?,
508        })
509    }
510}
511
512/// An account nonce: a counter the transaction kernel increments once per state-changing
513/// transaction.
514///
515/// Nonces compare as integers; they are produced by the account bindings and carry no
516/// arithmetic of their own.
517#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
518#[repr(transparent)]
519pub struct Nonce {
520    /// The raw field representation. Public only because component-model bindings construct
521    /// WIT records by field.
522    #[doc(hidden)]
523    pub inner: Felt,
524}
525
526impl Nonce {
527    /// Returns the nonce as a `u64` value.
528    #[inline]
529    pub fn as_u64(&self) -> u64 {
530        self.inner.as_canonical_u64()
531    }
532
533    /// Returns the nonce as a raw [`Felt`] for advanced use.
534    #[inline]
535    pub fn as_felt(&self) -> Felt {
536        self.inner
537    }
538}
539
540impl From<Nonce> for Felt {
541    #[inline]
542    fn from(value: Nonce) -> Self {
543        value.inner
544    }
545}
546
547/// A block height in the chain.
548///
549/// Block numbers compare as integers and are bounded to `u32` by the protocol. Kernel-returned
550/// heights are trusted; raw felts (e.g. read from note storage) convert via the validated
551/// [`TryFrom<Felt>`] implementation.
552#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
553#[repr(transparent)]
554pub struct BlockNumber {
555    /// The raw field representation. Public only because component-model bindings construct
556    /// WIT records by field.
557    #[doc(hidden)]
558    pub inner: Felt,
559}
560
561impl BlockNumber {
562    /// Returns the block number as a `u32` value.
563    ///
564    /// # Panics
565    ///
566    /// Panics if the wrapped felt exceeds the maximum block height (possible only for values
567    /// that bypassed validation, e.g. a WIT record lifted from a raw felt).
568    #[inline]
569    pub fn as_u32(&self) -> u32 {
570        // Compared in the felt domain: felt comparisons lower to VM intrinsics, which is much
571        // cheaper than u64 comparison libcalls.
572        assert!(
573            self.inner <= Felt::from_u32(u32::MAX),
574            "block number exceeds the maximum block height"
575        );
576        self.inner.as_canonical_u64() as u32
577    }
578
579    /// Returns the block number as a raw [`Felt`] for advanced use.
580    #[inline]
581    pub fn as_felt(&self) -> Felt {
582        self.inner
583    }
584}
585
586impl From<u32> for BlockNumber {
587    fn from(value: u32) -> Self {
588        Self {
589            inner: Felt::from_u32(value),
590        }
591    }
592}
593
594impl TryFrom<Felt> for BlockNumber {
595    type Error = &'static str;
596
597    fn try_from(value: Felt) -> Result<Self, Self::Error> {
598        if value.as_canonical_u64() > u32::MAX as u64 {
599            return Err("block number exceeds the maximum block height");
600        }
601        Ok(Self { inner: value })
602    }
603}
604
605impl From<BlockNumber> for Felt {
606    #[inline]
607    fn from(value: BlockNumber) -> Self {
608        value.inner
609    }
610}
611
612/// The partial hash of a storage slot name.
613///
614/// A slot id consists of two field elements: a `prefix` and a `suffix`.
615///
616/// Slot ids uniquely identify slots in account storage and are used by the host functions exposed
617/// via `miden::protocol::*`.
618#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
619pub struct StorageSlotId {
620    suffix: Felt,
621    prefix: Felt,
622}
623
624impl StorageSlotId {
625    /// Creates a new [`StorageSlotId`] from the provided felts.
626    ///
627    /// Note: this constructor takes `(suffix, prefix)` to match the values returned by
628    /// `miden_protocol::account::StorageSlotId::{suffix,prefix}`.
629    pub fn new(suffix: Felt, prefix: Felt) -> Self {
630        Self { suffix, prefix }
631    }
632
633    /// Creates a new [`StorageSlotId`] from the provided felts in host-call order.
634    ///
635    /// Host functions take the `prefix` first and then the `suffix`.
636    pub fn from_prefix_suffix(prefix: Felt, suffix: Felt) -> Self {
637        Self { suffix, prefix }
638    }
639
640    /// Returns the `(prefix, suffix)` pair in host-call order.
641    pub fn to_prefix_suffix(&self) -> (Felt, Felt) {
642        (self.prefix, self.suffix)
643    }
644
645    /// Returns the `(suffix, prefix)` pair in storage-slot order.
646    pub fn to_suffix_prefix(&self) -> (Felt, Felt) {
647        (self.suffix, self.prefix)
648    }
649
650    /// Returns the suffix of the [`StorageSlotId`].
651    pub fn suffix(&self) -> Felt {
652        self.suffix
653    }
654
655    /// Returns the prefix of the [`StorageSlotId`].
656    pub fn prefix(&self) -> Felt {
657        self.prefix
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use miden_stdlib_sys::{Felt, Word, felt};
664
665    use super::{
666        Asset, AssetAmount, AssetAmountError, BlockNumber, felt_from_padded_word,
667        padded_word_from_felt,
668    };
669
670    /// Ensures `padded_word_from_felt` zero-pads the trailing three limbs.
671    #[test]
672    fn padded_word_from_felt_zero_pads_trailing_limbs() {
673        assert_eq!(
674            padded_word_from_felt(felt!(7)),
675            Word::new([felt!(7), felt!(0), felt!(0), felt!(0)])
676        );
677    }
678
679    /// Ensures `felt_from_padded_word` rejects words with non-zero trailing padding.
680    #[test]
681    fn felt_from_padded_word_rejects_non_zero_padding() {
682        let err =
683            felt_from_padded_word(Word::new([felt!(7), felt!(1), felt!(0), felt!(0)])).unwrap_err();
684
685        assert_eq!(err, "expected zero padding in the trailing three felts");
686    }
687
688    /// Ensures the felt-padding helpers form a lossless roundtrip for scalar values.
689    #[test]
690    fn felt_padding_helpers_roundtrip() {
691        let value = felt!(42);
692
693        assert_eq!(felt_from_padded_word(padded_word_from_felt(value)), Ok(value));
694    }
695
696    /// Ensures amounts within the bound construct successfully and convert back losslessly.
697    #[test]
698    fn asset_amount_valid_amounts() {
699        assert_eq!(AssetAmount::new(0).unwrap().as_u64(), 0);
700        assert_eq!(AssetAmount::new(1000).unwrap().as_u64(), 1000);
701        assert_eq!(AssetAmount::new(AssetAmount::MAX_U64).unwrap(), AssetAmount::max());
702    }
703
704    /// Ensures amounts above the bound are rejected with the offending value.
705    #[test]
706    fn asset_amount_exceeds_max() {
707        assert_eq!(
708            AssetAmount::new(AssetAmount::MAX_U64 + 1),
709            Err(AssetAmountError::AmountTooBig(AssetAmount::MAX_U64 + 1))
710        );
711        assert_eq!(AssetAmount::new(u64::MAX), Err(AssetAmountError::AmountTooBig(u64::MAX)));
712    }
713
714    /// Ensures the maximum amount constant matches its documented value.
715    #[test]
716    fn asset_amount_max_value() {
717        assert_eq!(AssetAmount::MAX_U64, 2u64.pow(63) - 2u64.pow(31));
718        assert_eq!(AssetAmount::max().as_u64(), AssetAmount::MAX_U64);
719    }
720
721    /// Ensures the infallible conversions from small integer types.
722    #[test]
723    fn asset_amount_from_small_types() {
724        assert_eq!(AssetAmount::from(42u8).as_u64(), 42);
725        assert_eq!(AssetAmount::from(1000u16).as_u64(), 1000);
726        assert_eq!(AssetAmount::from(u32::MAX).as_u64(), u32::MAX as u64);
727    }
728
729    /// Ensures the fallible conversions from `u64` and `Felt` enforce the bound.
730    #[test]
731    fn asset_amount_try_from() {
732        assert!(AssetAmount::try_from(AssetAmount::MAX_U64).is_ok());
733        assert!(AssetAmount::try_from(AssetAmount::MAX_U64 + 1).is_err());
734        assert!(AssetAmount::try_from(Felt::new(AssetAmount::MAX_U64).unwrap()).is_ok());
735        assert!(AssetAmount::try_from(Felt::new(AssetAmount::MAX_U64 + 1).unwrap()).is_err());
736        // The largest canonical felt is far above the bound and must be rejected.
737        assert_eq!(
738            AssetAmount::try_from(Felt::new(Felt::ORDER - 1).unwrap()),
739            Err(AssetAmountError::AmountTooBig(Felt::ORDER - 1))
740        );
741    }
742
743    /// Ensures addition computes exact integer sums for in-range amounts.
744    #[test]
745    fn asset_amount_add() {
746        let a = AssetAmount::new(100).unwrap();
747        let b = AssetAmount::new(200).unwrap();
748
749        assert_eq!((a + b).as_u64(), 300);
750        assert_eq!(AssetAmount::ZERO + AssetAmount::ZERO, AssetAmount::ZERO);
751        assert_eq!(AssetAmount::max() + AssetAmount::ZERO, AssetAmount::max());
752    }
753
754    /// Ensures addition panics when the sum exceeds the maximum amount.
755    #[test]
756    #[should_panic(expected = "asset amount addition overflow")]
757    fn asset_amount_add_panics_on_overflow() {
758        let _ = AssetAmount::max() + AssetAmount::new(1).unwrap();
759    }
760
761    /// Ensures addition rejects an out-of-range left operand built via direct field assignment
762    /// instead of laundering it into a valid-looking sum; this operand would wrap the field to
763    /// zero if it were not rejected.
764    #[test]
765    #[should_panic(expected = "asset amount exceeds the maximum allowed amount")]
766    fn asset_amount_add_panics_on_forged_lhs() {
767        let wrapping = AssetAmount {
768            inner: Felt::new(Felt::ORDER - 1).unwrap(),
769        };
770
771        let _ = wrapping + AssetAmount::new(1).unwrap();
772    }
773
774    /// Ensures addition rejects an out-of-range right operand built via direct field
775    /// assignment (reported as an overflowing sum).
776    #[test]
777    #[should_panic(expected = "asset amount addition overflow")]
778    fn asset_amount_add_panics_on_forged_rhs() {
779        let forged = AssetAmount {
780            inner: Felt::new(AssetAmount::MAX_U64 + 1).unwrap(),
781        };
782
783        let _ = AssetAmount::new(1).unwrap() + forged;
784    }
785
786    /// Ensures subtraction computes exact integer differences for in-range amounts.
787    #[test]
788    fn asset_amount_sub() {
789        let a = AssetAmount::new(300).unwrap();
790        let b = AssetAmount::new(100).unwrap();
791
792        assert_eq!((a - b).as_u64(), 200);
793        assert_eq!(AssetAmount::ZERO - AssetAmount::ZERO, AssetAmount::ZERO);
794        assert_eq!(AssetAmount::max() - AssetAmount::max(), AssetAmount::ZERO);
795    }
796
797    /// Ensures subtraction panics when the subtrahend exceeds the minuend.
798    #[test]
799    #[should_panic(expected = "asset amount subtraction underflow")]
800    fn asset_amount_sub_panics_on_underflow() {
801        let _ = AssetAmount::ZERO - AssetAmount::new(1).unwrap();
802    }
803
804    /// Ensures subtraction rejects an out-of-range minuend built via direct field assignment,
805    /// which could otherwise produce an out-of-range result.
806    #[test]
807    #[should_panic(expected = "asset amount exceeds the maximum allowed amount")]
808    fn asset_amount_sub_panics_on_forged_minuend() {
809        let forged = AssetAmount {
810            inner: Felt::new(AssetAmount::MAX_U64 + 1).unwrap(),
811        };
812
813        let _ = forged - AssetAmount::new(1).unwrap();
814    }
815
816    /// Ensures the SDK amount arithmetic agrees with the off-chain protocol implementation
817    /// whenever the protocol operation succeeds (the SDK panics where the protocol errors).
818    #[test]
819    fn asset_amount_differential_vs_protocol() {
820        use miden_protocol::asset::AssetAmount as ProtocolAmount;
821
822        let values = [
823            0u64,
824            1,
825            2,
826            31,
827            u32::MAX as u64,
828            1 << 40,
829            AssetAmount::MAX_U64 / 2,
830            AssetAmount::MAX_U64 - 1,
831            AssetAmount::MAX_U64,
832        ];
833        for &a in &values {
834            for &b in &values {
835                let ours = (AssetAmount::new(a).unwrap(), AssetAmount::new(b).unwrap());
836                let theirs = (ProtocolAmount::new(a).unwrap(), ProtocolAmount::new(b).unwrap());
837
838                if let Ok(sum) = theirs.0 + theirs.1 {
839                    assert_eq!(
840                        (ours.0 + ours.1).as_u64(),
841                        sum.as_u64(),
842                        "sum mismatch for {a} + {b}"
843                    );
844                }
845
846                if let Ok(difference) = theirs.0 - theirs.1 {
847                    assert_eq!(
848                        (ours.0 - ours.1).as_u64(),
849                        difference.as_u64(),
850                        "difference mismatch for {a} - {b}"
851                    );
852                }
853            }
854        }
855    }
856
857    /// Ensures comparison follows canonical integer ordering.
858    #[test]
859    fn asset_amount_ordering() {
860        assert!(AssetAmount::new(1).unwrap() < AssetAmount::new(2).unwrap());
861        assert!(AssetAmount::max() > AssetAmount::ZERO);
862        assert_eq!(AssetAmount::default(), AssetAmount::ZERO);
863    }
864
865    /// Ensures the amount displays as a decimal integer.
866    #[test]
867    fn asset_amount_display() {
868        extern crate alloc;
869        use alloc::string::ToString;
870
871        assert_eq!(AssetAmount::new(12345).unwrap().to_string(), "12345");
872    }
873
874    /// Ensures the felt accessor and conversions roundtrip the underlying value.
875    #[test]
876    fn asset_amount_felt_roundtrip() {
877        let amount = AssetAmount::new(500).unwrap();
878
879        assert_eq!(amount.as_felt(), felt!(500));
880        assert_eq!(Felt::from(amount), felt!(500));
881        assert_eq!(u64::from(amount), 500);
882    }
883
884    /// Creates a raw fungible asset encoding (composition bits `0b01` in the key metadata byte)
885    /// for amount tests.
886    fn fungible_asset(amount: Felt) -> Asset {
887        Asset::new(
888            Word::new([felt!(0), felt!(0), felt!(1), felt!(0)]),
889            Word::new([amount, felt!(0), felt!(0), felt!(0)]),
890        )
891    }
892
893    /// Ensures the fungibility check discriminates by the composition parity.
894    #[test]
895    fn asset_is_fungible() {
896        let non_fungible = Asset::new(
897            Word::new([felt!(0), felt!(0), felt!(2), felt!(0)]),
898            Word::new([felt!(42), felt!(0), felt!(0), felt!(0)]),
899        );
900
901        assert!(fungible_asset(felt!(42)).is_fungible());
902        assert!(!non_fungible.is_fungible());
903    }
904
905    /// Ensures fungible asset amounts are decoded from valid key/value encodings.
906    #[test]
907    fn asset_amount_decodes_valid_fungible_assets() {
908        let asset = fungible_asset(felt!(42));
909        // Metadata byte 0b101: fungible composition with the callback flag set.
910        let callback_asset =
911            Asset::new(Word::new([felt!(0), felt!(0), felt!(5), felt!(0)]), asset.value);
912
913        assert_eq!(asset.amount(), AssetAmount::new(42).unwrap());
914        assert_eq!(callback_asset.amount(), AssetAmount::new(42).unwrap());
915    }
916
917    /// Ensures the amount accessor panics for non-fungible assets (even composition bits).
918    #[test]
919    #[should_panic(expected = "asset is not fungible")]
920    fn asset_amount_panics_on_non_fungible() {
921        let non_fungible = Asset::new(
922            Word::new([felt!(1), felt!(0), felt!(0), felt!(0)]),
923            Word::new([felt!(42), felt!(0), felt!(0), felt!(0)]),
924        );
925
926        let _ = non_fungible.amount();
927    }
928
929    /// Ensures the amount accessor panics when the amount exceeds the maximum.
930    #[test]
931    #[should_panic(expected = "asset amount exceeds the maximum allowed amount")]
932    fn asset_amount_panics_on_excessive_amount() {
933        let excessive_amount = fungible_asset(Felt::new(AssetAmount::MAX_U64 + 1).unwrap());
934
935        let _ = excessive_amount.amount();
936    }
937
938    /// Ensures block-number felts validate against the `u32` protocol bound.
939    #[test]
940    fn block_number_try_from_felt_bounds() {
941        let max = Felt::new(u32::MAX as u64).unwrap();
942
943        assert_eq!(BlockNumber::try_from(max).unwrap().as_u32(), u32::MAX);
944        assert!(BlockNumber::try_from(Felt::new(u32::MAX as u64 + 1).unwrap()).is_err());
945    }
946
947    /// Ensures `as_u32` refuses to truncate an out-of-range felt smuggled in through the public
948    /// WIT-record field.
949    #[test]
950    #[should_panic(expected = "block number exceeds the maximum block height")]
951    fn block_number_as_u32_panics_on_out_of_range_felt() {
952        let forged = BlockNumber {
953            inner: Felt::new(u32::MAX as u64 + 1).unwrap(),
954        };
955
956        let _ = forged.as_u32();
957    }
958}