use miden_protocol::account::AccountId;
use miden_protocol::asset::{
AssetCallbackFlag,
AssetComposition,
AssetId,
AssetVaultKey,
FungibleAsset,
NonFungibleAsset,
NonFungibleAssetDetails,
};
use miden_protocol::errors::MasmError;
use miden_protocol::errors::protocol::ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS;
use miden_protocol::errors::tx_kernel::{
ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT,
ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO,
ERR_FUNGIBLE_ASSET_KEY_COMPOSITION_MUST_BE_FUNGIBLE,
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_COMPOSITION_MUST_BE_NON_FUNGIBLE,
ERR_VAULT_ASSET_METADATA_NOT_U32,
ERR_VAULT_ASSET_METADATA_UNKNOWN_COMPOSITION,
};
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(())
}
const METADATA_BYTE_NONE: u64 = 0;
const METADATA_BYTE_FUNGIBLE: u64 = AssetComposition::Fungible as u64;
fn key_suffix_with_metadata(account_id: AccountId, metadata_byte: u64) -> Felt {
Felt::try_from(account_id.suffix().as_canonical_u64() | metadata_byte)
.expect("metadata byte only occupies the lower 8 bits")
}
#[rstest::rstest]
#[case::account_is_not_non_fungible_faucet(
ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?,
AssetId::default(),
METADATA_BYTE_FUNGIBLE,
ERR_NON_FUNGIBLE_ASSET_KEY_COMPOSITION_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)),
METADATA_BYTE_NONE,
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)),
METADATA_BYTE_NONE,
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] metadata_byte: u64,
#[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 = key_suffix_with_metadata(account_id, metadata_byte),
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(),
METADATA_BYTE_NONE,
ERR_FUNGIBLE_ASSET_KEY_COMPOSITION_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(),
METADATA_BYTE_FUNGIBLE,
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(),
METADATA_BYTE_FUNGIBLE,
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]),
METADATA_BYTE_FUNGIBLE,
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.as_u64() + 1, 0, 0, 0])?,
METADATA_BYTE_FUNGIBLE,
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] metadata_byte: u64,
#[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 = key_suffix_with_metadata(account_id, metadata_byte),
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::valid_none(0, None)]
#[case::valid_fungible(METADATA_BYTE_FUNGIBLE, None)]
#[case::valid_custom(AssetComposition::Custom as u64, None)]
#[case::valid_callbacks_enabled((AssetCallbackFlag::Enabled as u64) << 2, None)]
#[case::not_u32(u32::MAX as u64 + 1, Some(ERR_VAULT_ASSET_METADATA_NOT_U32))]
#[case::not_u8(u16::MAX as u64, Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS))]
#[case::reserved_bits_set(0b1000, Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS))]
#[case::unknown_composition(0b011, Some(ERR_VAULT_ASSET_METADATA_UNKNOWN_COMPOSITION))]
#[tokio::test]
async fn test_validate_asset_metadata(
#[case] asset_metadata: u64,
#[case] expected_err: Option<MasmError>,
) -> anyhow::Result<()> {
let code = format!(
"
use $kernel::asset
begin
push.{asset_metadata}
exec.asset::validate_metadata
end
"
);
let exec_result = CodeExecutor::with_default_host().run(&code).await;
match expected_err {
Some(err) => assert_execution_error!(exec_result, err),
None => {
exec_result.expect("validate_metadata should accept valid metadata");
},
}
Ok(())
}
#[rstest::rstest]
#[case::fungible_without_callbacks(AssetComposition::Fungible, AssetCallbackFlag::Disabled)]
#[case::non_fungible_with_callbacks(AssetComposition::None, AssetCallbackFlag::Enabled)]
#[tokio::test]
async fn test_key_to_asset_metadata(
#[case] composition: AssetComposition,
#[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, composition, callbacks)?;
let code = format!(
"
use $kernel::asset
begin
push.{ASSET_KEY}
exec.asset::key_to_callbacks_enabled
# => [callbacks_enabled, ASSET_KEY]
movdn.4
exec.asset::key_to_composition
# => [asset_composition, ASSET_KEY, callbacks_enabled]
movdn.4 dropw
# => [asset_composition, callbacks_enabled]
# truncate stack
swapw dropw
# => [asset_composition, 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(),
composition.as_u8() as u64,
"MASM asset::key_to_composition returned wrong value for {composition:?}"
);
assert_eq!(
exec_output.get_stack_element(1).as_canonical_u64(),
callbacks.as_u8() as u64,
"MASM asset::key_to_callbacks_enabled returned wrong value for {callbacks:?}"
);
Ok(())
}