miden-testing 0.14.3

Miden protocol testing tools
Documentation
use miden_protocol::account::AccountId;
use miden_protocol::asset::{
    AssetCallbackFlag,
    AssetId,
    AssetVaultKey,
    FungibleAsset,
    NonFungibleAsset,
    NonFungibleAssetDetails,
};
use miden_protocol::errors::MasmError;
use miden_protocol::errors::tx_kernel::{
    ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT,
    ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE,
    ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO,
    ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO,
    ERR_NON_FUNGIBLE_ASSET_ID_PREFIX_MUST_MATCH_HASH1,
    ERR_NON_FUNGIBLE_ASSET_ID_SUFFIX_MUST_MATCH_HASH0,
    ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE,
};
use miden_protocol::testing::account_id::{
    ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
    ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
    ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
};
use miden_protocol::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA};
use miden_protocol::{Felt, Word};

use crate::executor::CodeExecutor;
use crate::kernel_tests::tx::ExecutionOutputExt;
use crate::{TransactionContextBuilder, assert_execution_error};

#[tokio::test]
async fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> {
    let tx_context =
        TransactionContextBuilder::with_fungible_faucet(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)
            .build()?;
    let expected_asset = FungibleAsset::new(tx_context.account().id(), FUNGIBLE_ASSET_AMOUNT)?;

    let code = format!(
        "
        use $kernel::prologue
        use miden::protocol::faucet

        begin
            exec.prologue::prepare_transaction

            # create fungible asset
            push.{FUNGIBLE_ASSET_AMOUNT}
            exec.faucet::create_fungible_asset
            # => [ASSET_KEY, ASSET_VALUE]

            # truncate the stack
            exec.::miden::core::sys::truncate_stack
        end
        "
    );

    let exec_output = &tx_context.execute_code(&code).await?;

    assert_eq!(exec_output.get_stack_word(0), expected_asset.to_key_word());
    assert_eq!(exec_output.get_stack_word(4), expected_asset.to_value_word());

    Ok(())
}

#[tokio::test]
async fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> {
    let tx_context =
        TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into())
            .build()?;

    let non_fungible_asset_details = NonFungibleAssetDetails::new(
        NonFungibleAsset::mock_issuer(),
        NON_FUNGIBLE_ASSET_DATA.to_vec(),
    )?;
    let non_fungible_asset = NonFungibleAsset::new(&non_fungible_asset_details)?;

    let code = format!(
        "
        use $kernel::prologue
        use miden::protocol::faucet

        begin
            exec.prologue::prepare_transaction

            # push non-fungible asset data hash onto the stack
            push.{NON_FUNGIBLE_ASSET_DATA_HASH}
            exec.faucet::create_non_fungible_asset

            # truncate the stack
            exec.::miden::core::sys::truncate_stack
        end
        ",
        NON_FUNGIBLE_ASSET_DATA_HASH = non_fungible_asset.to_value_word(),
    );

    let exec_output = &tx_context.execute_code(&code).await?;

    assert_eq!(exec_output.get_stack_word(0), non_fungible_asset.to_key_word());
    assert_eq!(exec_output.get_stack_word(4), non_fungible_asset.to_value_word());

    Ok(())
}

#[rstest::rstest]
#[case::account_is_not_non_fungible_faucet(
    ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?,
    AssetId::default(),
    ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE
)]
#[case::asset_id_suffix_mismatch(
    ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?,
    AssetId::new(Felt::from(0u32), Felt::from(3u32)),
    ERR_NON_FUNGIBLE_ASSET_ID_SUFFIX_MUST_MATCH_HASH0
)]
#[case::asset_id_prefix_mismatch(
    ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?,
    AssetId::new(Felt::from(2u32), Felt::from(0u32)),
    ERR_NON_FUNGIBLE_ASSET_ID_PREFIX_MUST_MATCH_HASH1
)]
#[tokio::test]
async fn test_validate_non_fungible_asset(
    #[case] account_id: AccountId,
    #[case] asset_id: AssetId,
    #[case] expected_err: MasmError,
) -> anyhow::Result<()> {
    let code = format!(
        "
        use $kernel::non_fungible_asset

        begin
            # a random asset value
            push.[2, 3, 4, 5]
            # => [hash0 = 2, hash1 = 3, 4, 5]

            push.{account_id_prefix}
            push.{account_id_suffix}
            push.{asset_id_prefix}
            push.{asset_id_suffix}
            # => [ASSET_KEY, ASSET_VALUE]

            exec.non_fungible_asset::validate

            # truncate the stack
            swapdw dropw dropw
        end
        ",
        asset_id_suffix = asset_id.suffix(),
        asset_id_prefix = asset_id.prefix(),
        account_id_suffix = account_id.suffix(),
        account_id_prefix = account_id.prefix().as_felt(),
    );

    let exec_result = CodeExecutor::with_default_host().run(&code).await;

    assert_execution_error!(exec_result, expected_err);

    Ok(())
}

#[rstest::rstest]
#[case::account_is_not_fungible_faucet(
    ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?,
    AssetId::default(),
    Word::empty(),
    ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE
)]
#[case::asset_id_suffix_is_non_zero(
    ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?,
    AssetId::new(Felt::from(1u32), Felt::from(0u32)),
    Word::empty(),
    ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO
)]
#[case::asset_id_prefix_is_non_zero(
    ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?,
    AssetId::new(Felt::from(0u32), Felt::from(1u32)),
    Word::empty(),
    ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO
)]
#[case::non_amount_value_is_non_zero(
    ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?,
    AssetId::default(),
    Word::from([0, 1, 0, 0u32]),
    ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO
)]
#[case::amount_exceeds_max(
    ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?,
    AssetId::default(),
    Word::try_from([FungibleAsset::MAX_AMOUNT + 1, 0, 0, 0])?,
    ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT
)]
#[tokio::test]
async fn test_validate_fungible_asset(
    #[case] account_id: AccountId,
    #[case] asset_id: AssetId,
    #[case] asset_value: Word,
    #[case] expected_err: MasmError,
) -> anyhow::Result<()> {
    let code = format!(
        "
        use $kernel::fungible_asset

        begin
            push.{ASSET_VALUE}
            push.{account_id_prefix}
            push.{account_id_suffix}
            push.{asset_id_prefix}
            push.{asset_id_suffix}
            # => [ASSET_KEY, ASSET_VALUE]

            exec.fungible_asset::validate

            # truncate the stack
            swapdw dropw dropw
        end
        ",
        asset_id_suffix = asset_id.suffix(),
        asset_id_prefix = asset_id.prefix(),
        account_id_suffix = account_id.suffix(),
        account_id_prefix = account_id.prefix().as_felt(),
        ASSET_VALUE = asset_value,
    );

    let exec_result = CodeExecutor::with_default_host().run(&code).await;

    assert_execution_error!(exec_result, expected_err);

    Ok(())
}

#[rstest::rstest]
#[case::without_callbacks(AssetCallbackFlag::Disabled)]
#[case::with_callbacks(AssetCallbackFlag::Enabled)]
#[tokio::test]
async fn test_key_to_asset_metadata(#[case] callbacks: AssetCallbackFlag) -> anyhow::Result<()> {
    let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
    let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, callbacks)?;

    let code = format!(
        "
        use $kernel::asset

        begin
            push.{ASSET_KEY}
            exec.asset::key_to_callbacks_enabled
            # => [callbacks_enabled, ASSET_KEY]

            # truncate stack
            swapw dropw swap drop
            # => [callbacks_enabled]
        end
        ",
        ASSET_KEY = vault_key.to_word(),
    );

    let exec_output = CodeExecutor::with_default_host().run(&code).await?;

    assert_eq!(
        exec_output.get_stack_element(0).as_canonical_u64(),
        callbacks.as_u8() as u64,
        "MASM key_to_asset_category returned wrong value for {callbacks:?}"
    );

    Ok(())
}