Skip to main content

miden_testing/
asserts.rs

1//! Assertion macro for note-lifecycle checks in tests.
2
3use alloc::vec::Vec;
4
5use miden_protocol::account::AccountId;
6use miden_protocol::asset::Asset;
7use miden_protocol::note::NoteType;
8use miden_protocol::transaction::ExecutedTransaction;
9
10/// Spec for [`assert_note_created!`]. Fields left as `None` are skipped.
11#[doc(hidden)]
12#[derive(Default, Debug, Clone)]
13pub struct OutputNoteSpec {
14    pub note_type: Option<NoteType>,
15    pub sender: Option<AccountId>,
16    pub assets: Option<Vec<Asset>>,
17}
18
19/// Returns `true` if at least one output note in `tx` matches `spec`.
20#[doc(hidden)]
21pub fn check_output_note_created(tx: &ExecutedTransaction, spec: &OutputNoteSpec) -> bool {
22    tx.output_notes().iter().any(|note| {
23        if let Some(expected) = spec.note_type
24            && note.metadata().note_type() != expected
25        {
26            return false;
27        }
28        if let Some(expected) = spec.sender
29            && note.metadata().sender() != expected
30        {
31            return false;
32        }
33        if let Some(expected) = spec.assets.as_ref() {
34            let actual = note.assets();
35            if actual.num_assets() != expected.len() {
36                return false;
37            }
38            // Each actual matches at most once (otherwise [A,A] would match [A,B]).
39            let mut consumed = vec![false; expected.len()];
40            let matched = expected.iter().all(|exp| {
41                let slot = actual.iter().enumerate().find(|(i, a)| !consumed[*i] && *a == exp);
42                if let Some((i, _)) = slot {
43                    consumed[i] = true;
44                    true
45                } else {
46                    false
47                }
48            });
49            if !matched {
50                return false;
51            }
52        }
53        true
54    })
55}
56
57/// Asserts the tx emitted a note matching the spec. Fields are optional; unset ones are skipped.
58///
59/// # Example
60/// ```ignore
61/// use miden_testing::assert_note_created;
62/// use miden_protocol::note::NoteType;
63///
64/// assert_note_created!(
65///     executed_tx,
66///     note_type: NoteType::Public,
67///     sender: faucet.id(),
68///     assets: [FungibleAsset::new(faucet.id(), amount)?.into()],
69/// );
70/// ```
71#[macro_export]
72macro_rules! assert_note_created {
73    ($tx:expr $(, $key:ident : $val:expr)* $(,)?) => {{
74        #[allow(unused_mut)]
75        let mut spec = $crate::asserts::OutputNoteSpec::default();
76        $(
77            $crate::__assert_note_created_field!(spec, $key, $val);
78        )*
79        let tx: &::miden_protocol::transaction::ExecutedTransaction = &$tx;
80        assert!(
81            $crate::asserts::check_output_note_created(tx, &spec),
82            "no output note matches spec: {:?}\n  tx produced {} output note(s)",
83            spec,
84            tx.output_notes().num_notes(),
85        );
86    }};
87}
88
89#[doc(hidden)]
90#[macro_export]
91macro_rules! __assert_note_created_field {
92    ($spec:ident,note_type, $val:expr) => {
93        $spec.note_type = ::core::option::Option::Some($val);
94    };
95    ($spec:ident,sender, $val:expr) => {
96        $spec.sender = ::core::option::Option::Some($val);
97    };
98    ($spec:ident,assets, $val:expr) => {
99        $spec.assets = ::core::option::Option::Some(
100            ::core::iter::IntoIterator::into_iter($val)
101                .map(::core::convert::Into::into)
102                .collect::<::alloc::vec::Vec<::miden_protocol::asset::Asset>>(),
103        );
104    };
105    ($spec:ident, $key:ident, $val:expr) => {
106        ::core::compile_error!(concat!(
107            "unknown field in assert_note_created!: `",
108            stringify!($key),
109            "`. Supported fields: note_type, sender, assets",
110        ));
111    };
112}