extern crate alloc;
use alloc::sync::Arc;
use core::slice;
use std::collections::BTreeSet;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{
Account,
AccountBuilder,
AccountId,
AccountProcedureRoot,
AccountType,
AssetCallbackFlag,
};
use miden_protocol::assembly::DefaultSourceManager;
use miden_protocol::asset::{Asset, AssetAmount, FungibleAsset, TokenSymbol};
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachments,
NoteDetailsCommitment,
NoteId,
NoteMetadata,
NoteRecipient,
NoteScript,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
use miden_protocol::transaction::{ExecutedTransaction, RawOutputNote};
use miden_protocol::{Felt, Word};
use miden_standards::account::access::{Authority, Ownable2Step, Pausable};
use miden_standards::account::faucets::{FungibleFaucet, TokenName};
use miden_standards::account::policies::{
BurnAllowAll,
BurnOwnerOnly,
BurnPolicy,
MinBurnAmount,
MintPolicy,
TokenPolicyManager,
TransferPolicy,
};
use miden_standards::code_builder::CodeBuilder;
use miden_standards::errors::standards::{
ERR_BURN_AMOUNT_BELOW_MIN_BURN_AMOUNT,
ERR_BURN_POLICY_ROOT_NOT_ALLOWED,
ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY,
ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY,
ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT,
ERR_MINT_POLICY_ROOT_NOT_ALLOWED,
ERR_SENDER_NOT_OWNER,
};
use miden_standards::note::{BurnNote, MintNote, MintNoteStorage, P2idNote, StandardNote};
use miden_standards::testing::note::NoteBuilder;
use miden_testing::{
AccountState,
Auth,
MockChain,
MockChainBuilder,
assert_note_created,
assert_transaction_executor_error,
};
use rand::RngExt;
use crate::{get_note_with_fungible_asset_and_script, prove_and_verify_transaction};
pub struct FaucetTestParams {
pub recipient: Word,
pub tag: NoteTag,
pub note_type: NoteType,
pub amount: Felt,
}
pub fn create_mint_script_code(params: &FaucetTestParams, faucet_id: AccountId) -> String {
format!(
"
@transaction_script
pub proc main
push.{recipient}
push.{note_type}
push.{tag}
push.{amount}
push.{faucet_id_prefix}
push.{faucet_id_suffix}
# => [faucet_id_suffix, faucet_id_prefix, amount, tag, note_type, RECIPIENT, ...]
exec.::miden::standards::assets::fungible_asset::create
# => [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT, ...]
call.::miden::standards::faucets::fungible::mint_and_send
# => [note_idx, pad(15)]
# truncate the stack
dropw dropw dropw dropw
end
",
note_type = params.note_type as u8,
recipient = params.recipient,
tag = u32::from(params.tag),
amount = params.amount,
faucet_id_suffix = faucet_id.suffix(),
faucet_id_prefix = faucet_id.prefix().as_felt(),
)
}
pub async fn execute_mint_transaction(
mock_chain: &mut MockChain,
faucet: Account,
params: &FaucetTestParams,
) -> anyhow::Result<ExecutedTransaction> {
let source_manager = Arc::new(DefaultSourceManager::default());
let tx_script_code = create_mint_script_code(params, faucet.id());
let tx_script = CodeBuilder::with_source_manager(source_manager.clone())
.compile_tx_script(tx_script_code)?;
let tx_context = mock_chain
.build_tx_context(faucet, &[], &[])?
.tx_script(tx_script)
.with_source_manager(source_manager)
.build()?;
Ok(tx_context.execute().await?)
}
pub fn verify_minted_output_note(
executed_transaction: &ExecutedTransaction,
faucet: &Account,
params: &FaucetTestParams,
) -> anyhow::Result<()> {
let output_note = executed_transaction.output_notes().get_note(0).clone();
let fungible_asset: Asset =
FungibleAsset::new(faucet.id(), params.amount.as_canonical_u64())?.into();
let assets = NoteAssets::new(vec![fungible_asset])?;
let partial_metadata =
PartialNoteMetadata::new(faucet.id(), params.note_type).with_tag(params.tag);
let metadata = NoteMetadata::new(partial_metadata, &NoteAttachments::default());
let details_commitment =
NoteDetailsCommitment::from_raw_commitments(params.recipient, assets.commitment());
let id = NoteId::new(details_commitment, &metadata);
assert_eq!(output_note.id(), id);
assert_eq!(output_note.metadata().partial_metadata(), &partial_metadata);
Ok(())
}
fn compile_note_script(code: &str) -> anyhow::Result<NoteScript> {
Ok(CodeBuilder::default().compile_note_script(code)?)
}
async fn execute_faucet_note_script(
mock_chain: &MockChain,
faucet_id: AccountId,
sender_account_id: AccountId,
note_script: NoteScript,
rng_seed: u32,
) -> anyhow::Result<Result<ExecutedTransaction, miden_tx::TransactionExecutorError>> {
let source_manager = Arc::new(DefaultSourceManager::default());
let mut rng = RandomCoin::new([Felt::from(rng_seed); 4].into());
let note = NoteBuilder::new(sender_account_id, &mut rng)
.note_type(NoteType::Private)
.script(note_script)
.build()?;
let tx_context = mock_chain
.build_tx_context(faucet_id, &[], &[note])?
.with_source_manager(source_manager)
.build()?;
Ok(tx_context.execute().await)
}
fn create_set_burn_policy_note_script(policy_root: Word) -> String {
format!(
r#"
use miden::standards::faucets::policies::policy_manager
@note_script
pub proc main
padw padw padw
push.{policy_root}
call.policy_manager::set_burn_policy
dropw dropw dropw dropw
end
"#
)
}
fn create_policy_getters_note_script(
mint_root: AccountProcedureRoot,
burn_root: AccountProcedureRoot,
send_root: AccountProcedureRoot,
receive_root: AccountProcedureRoot,
) -> String {
let mint_root = mint_root.as_word();
let burn_root = burn_root.as_word();
let send_root = send_root.as_word();
let receive_root = receive_root.as_word();
format!(
r#"
use miden::standards::faucets::policies::policy_manager
@note_script
pub proc main
padw padw padw padw
call.policy_manager::get_mint_policy
# => [MINT_POLICY_ROOT, pad(12)]
push.{mint_root}
assert_eqw.err="get_mint_policy returned an unexpected root or violated the call ABI"
dropw dropw dropw
padw padw padw padw
call.policy_manager::get_burn_policy
# => [BURN_POLICY_ROOT, pad(12)]
push.{burn_root}
assert_eqw.err="get_burn_policy returned an unexpected root or violated the call ABI"
dropw dropw dropw
padw padw padw padw
call.policy_manager::get_send_policy
# => [SEND_POLICY_ROOT, pad(12)]
push.{send_root}
assert_eqw.err="get_send_policy returned an unexpected root or violated the call ABI"
dropw dropw dropw
padw padw padw padw
call.policy_manager::get_receive_policy
# => [RECEIVE_POLICY_ROOT, pad(12)]
push.{receive_root}
assert_eqw.err="get_receive_policy returned an unexpected root or violated the call ABI"
dropw dropw dropw
end
"#
)
}
fn build_network_faucet_with_burn_switching(
builder: &mut MockChainBuilder,
token_symbol: &str,
max_supply: u64,
owner: AccountId,
token_supply: u64,
mint_policy: MintPolicy,
) -> anyhow::Result<Account> {
let name = TokenName::new(token_symbol)?;
let symbol = TokenSymbol::new(token_symbol)?;
let max_supply = AssetAmount::new(max_supply)?;
let token_supply = AssetAmount::new(token_supply)?;
let faucet = FungibleFaucet::builder()
.name(name)
.symbol(symbol)
.decimals(10)
.max_supply(max_supply)
.token_supply(token_supply)
.build()?;
let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(mint_policy)
.active_burn_policy(BurnPolicy::allow_all())
.allowed_burn_policy(BurnPolicy::owner_only())
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();
let account_builder = AccountBuilder::new(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_component(faucet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_asset_callbacks(AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()))
.with_components(token_policy_manager)
.with_component(Pausable::unpaused());
builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
}
fn build_existing_faucet_with_reserved_only_transfer_policy(
builder: &mut MockChainBuilder,
token_symbol: &str,
max_supply: u64,
owner: AccountId,
) -> anyhow::Result<Account> {
let name = TokenName::new(token_symbol)?;
let symbol = TokenSymbol::new(token_symbol)?;
let max_supply = AssetAmount::new(max_supply)?;
let faucet = FungibleFaucet::builder()
.name(name)
.symbol(symbol)
.decimals(10)
.max_supply(max_supply)
.build()?;
let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.active_burn_policy(BurnPolicy::allow_all())
.allowed_send_policy(TransferPolicy::allow_all())
.allowed_receive_policy(TransferPolicy::allow_all())
.build();
let account_builder = AccountBuilder::new(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_component(faucet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_asset_callbacks(AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()))
.with_components(token_policy_manager);
builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
}
fn build_network_faucet_with_min_burn_amount(
builder: &mut MockChainBuilder,
token_symbol: &str,
max_supply: u64,
owner: AccountId,
token_supply: u64,
min_burn_amount: u64,
) -> anyhow::Result<Account> {
let name = TokenName::new(token_symbol)?;
let symbol = TokenSymbol::new(token_symbol)?;
let max_supply = AssetAmount::new(max_supply)?;
let token_supply = AssetAmount::new(token_supply)?;
let min_burn_amount = AssetAmount::new(min_burn_amount)?;
let faucet = FungibleFaucet::builder()
.name(name)
.symbol(symbol)
.decimals(10)
.max_supply(max_supply)
.token_supply(token_supply)
.build()?;
let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::min_burn_amount(min_burn_amount))
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();
let account_builder = AccountBuilder::new(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_component(faucet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_asset_callbacks(AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()))
.with_components(token_policy_manager)
.with_component(Pausable::unpaused());
builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
}
fn create_set_min_burn_amount_note_script(new_min_burn_amount: u64) -> String {
format!(
r#"
use miden::standards::faucets::policies::burn::min_burn_amount
@note_script
pub proc main
padw padw padw push.0.0.0
push.{new_min_burn_amount}
call.min_burn_amount::set_min_burn_amount
dropw dropw dropw dropw
end
"#
)
}
#[tokio::test]
async fn minting_fungible_asset_on_existing_faucet_succeeds() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet = builder.add_existing_basic_faucet(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
"TST",
200,
None,
)?;
let mut mock_chain = builder.build()?;
let params = FaucetTestParams {
recipient: Word::from([0, 1, 2, 3u32]),
tag: NoteTag::default(),
note_type: NoteType::Private,
amount: Felt::new_unchecked(100),
};
let executed_transaction =
execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?;
verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?;
Ok(())
}
#[tokio::test]
async fn minting_on_reserved_only_transfer_policy_faucet_enables_callbacks() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = build_existing_faucet_with_reserved_only_transfer_policy(
&mut builder,
"RSV",
1000,
owner_account_id,
)?;
let mut mock_chain = builder.build()?;
let params = FaucetTestParams {
recipient: Word::from([0, 1, 2, 3u32]),
tag: NoteTag::default(),
note_type: NoteType::Private,
amount: Felt::new_unchecked(100),
};
let executed_transaction =
execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?;
verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?;
Ok(())
}
#[tokio::test]
async fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet = builder.add_existing_basic_faucet(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
"TST",
200,
None,
)?;
let mock_chain = builder.build()?;
let recipient = Word::from([0, 1, 2, 3u32]);
let tag = Felt::new_unchecked(4);
let amount = Felt::new_unchecked(250);
let tx_script_code = format!(
"
@transaction_script
pub proc main
push.{recipient}
push.{note_type}
push.{tag}
push.{amount}
push.{faucet_id_prefix}
push.{faucet_id_suffix}
# => [faucet_id_suffix, faucet_id_prefix, amount, tag, note_type, RECIPIENT, ...]
exec.::miden::standards::assets::fungible_asset::create
# => [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT, ...]
call.::miden::standards::faucets::fungible::mint_and_send
# => [note_idx, pad(15)]
# truncate the stack
dropw dropw dropw dropw
end
",
note_type = NoteType::Private as u8,
recipient = recipient,
faucet_id_suffix = faucet.id().suffix(),
faucet_id_prefix = faucet.id().prefix().as_felt(),
);
let tx_script = CodeBuilder::default().compile_tx_script(tx_script_code)?;
let tx = mock_chain
.build_tx_context(faucet.id(), &[], &[])?
.tx_script(tx_script)
.build()?
.execute()
.await;
assert_transaction_executor_error!(tx, ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY);
Ok(())
}
#[tokio::test]
async fn minting_fungible_asset_on_new_faucet_succeeds() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet = builder.create_new_faucet(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
"TST",
200,
)?;
let mut mock_chain = builder.build()?;
let params = FaucetTestParams {
recipient: Word::from([0, 1, 2, 3u32]),
tag: NoteTag::default(),
note_type: NoteType::Private,
amount: Felt::new_unchecked(100),
};
let executed_transaction =
execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?;
verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?;
Ok(())
}
#[tokio::test]
async fn prove_burning_fungible_asset_on_existing_faucet_succeeds() -> anyhow::Result<()> {
let max_supply = 200u32;
let token_supply = 100u32;
let mut builder = MockChain::builder();
let faucet = builder.add_existing_basic_faucet(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
"TST",
max_supply.into(),
Some(token_supply.into()),
)?;
let fungible_asset = FungibleAsset::new(faucet.id(), 100).unwrap();
let burn_note_script_code = "
# burn the asset
@note_script
pub proc main
dropw
# => []
call.::miden::standards::faucets::fungible::receive_and_burn
# => [pad(16)]
end
";
let note = get_note_with_fungible_asset_and_script(fungible_asset, burn_note_script_code);
builder.add_output_note(RawOutputNote::Full(note.clone()));
let mock_chain = builder.build()?;
let token_metadata = FungibleFaucet::try_from(faucet.storage())?;
assert_eq!(token_metadata.max_supply(), AssetAmount::from(max_supply));
assert_eq!(token_metadata.token_supply(), AssetAmount::from(token_supply));
let executed_transaction = mock_chain
.build_tx_context(faucet.id(), &[note.id()], &[])?
.build()?
.execute()
.await?;
assert_eq!(
executed_transaction.account_patch().final_nonce(),
Some(faucet.nonce() + Felt::ONE)
);
assert_eq!(executed_transaction.input_notes().get_note(0).id(), note.id());
prove_and_verify_transaction(executed_transaction.clone()).await?;
Ok(())
}
#[tokio::test]
async fn faucet_burn_fungible_asset_fails_amount_exceeds_token_supply() -> anyhow::Result<()> {
let max_supply = 200u32;
let token_supply = 50u32;
let mut builder = MockChain::builder();
let faucet = builder.add_existing_basic_faucet(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
"TST",
max_supply.into(),
Some(token_supply.into()),
)?;
let burn_amount = 100u64;
let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap();
let burn_note_script_code = "
# burn the asset
@note_script
pub proc main
dropw
# => []
call.::miden::standards::faucets::fungible::receive_and_burn
# => [pad(16)]
end
";
let note = get_note_with_fungible_asset_and_script(fungible_asset, burn_note_script_code);
builder.add_output_note(RawOutputNote::Full(note.clone()));
let mock_chain = builder.build()?;
let tx = mock_chain
.build_tx_context(faucet.id(), &[note.id()], &[])?
.build()?
.execute()
.await;
assert_transaction_executor_error!(tx, ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY);
Ok(())
}
#[tokio::test]
async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet = builder.add_existing_basic_faucet(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
"TST",
200,
None,
)?;
let recipient_account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
let amount = Felt::new_unchecked(75);
let tag = NoteTag::default();
let note_type = NoteType::Public;
let output_note_script_code = "@note_script pub proc main push.1 drop end";
let source_manager = Arc::new(DefaultSourceManager::default());
let output_note_script = CodeBuilder::with_source_manager(source_manager.clone())
.compile_note_script(output_note_script_code)?;
let serial_num = Word::default();
let target_account_suffix = recipient_account_id.suffix();
let target_account_prefix = recipient_account_id.prefix().as_felt();
let note_storage = NoteStorage::new(vec![
target_account_suffix,
target_account_prefix,
Felt::ZERO,
Felt::ZERO,
Felt::ZERO,
Felt::ONE,
Felt::ZERO,
])?;
let note_recipient =
NoteRecipient::new(serial_num, output_note_script.clone(), note_storage.clone());
let output_script_root = note_recipient.script().root();
let asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let metadata = PartialNoteMetadata::new(faucet.id(), note_type).with_tag(tag);
let expected_note = Note::new(NoteAssets::new(vec![asset.into()])?, metadata, note_recipient);
let trigger_note_script_code = format!(
"
use miden::protocol::note
@note_script
pub proc main
# Build recipient hash from SERIAL_NUM, SCRIPT_ROOT, and STORAGE_COMMITMENT
push.{script_root}
# => [SCRIPT_ROOT]
push.{serial_num}
# => [SERIAL_NUM, SCRIPT_ROOT]
# Store note storage in memory
push.{input0} mem_store.0
push.{input1} mem_store.1
push.{input2} mem_store.2
push.{input3} mem_store.3
push.{input4} mem_store.4
push.{input5} mem_store.5
push.{input6} mem_store.6
push.7 push.0
# => [storage_ptr, num_storage_items = 7, SERIAL_NUM, SCRIPT_ROOT]
exec.note::compute_and_store_recipient
# => [RECIPIENT]
# Now call mint with the computed recipient
push.{note_type}
push.{tag}
push.{amount}
push.{faucet_id_prefix}
push.{faucet_id_suffix}
# => [faucet_id_suffix, faucet_id_prefix, amount, tag, note_type, RECIPIENT]
exec.::miden::standards::assets::fungible_asset::create
# => [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT]
call.::miden::standards::faucets::fungible::mint_and_send
# => [note_idx, pad(15)]
# Truncate the stack
dropw dropw dropw dropw
end
",
note_type = note_type as u8,
input0 = note_storage.items()[0],
input1 = note_storage.items()[1],
input2 = note_storage.items()[2],
input3 = note_storage.items()[3],
input4 = note_storage.items()[4],
input5 = note_storage.items()[5],
input6 = note_storage.items()[6],
script_root = output_script_root,
serial_num = serial_num,
tag = u32::from(tag),
amount = amount,
faucet_id_suffix = faucet.id().suffix(),
faucet_id_prefix = faucet.id().prefix().as_felt(),
);
let mut rng = RandomCoin::new([Felt::from(1u32); 4].into());
let trigger_note = NoteBuilder::new(faucet.id(), &mut rng)
.note_type(NoteType::Private)
.tag(NoteTag::default().into())
.serial_number(Word::from([1, 2, 3, 4u32]))
.code(trigger_note_script_code)
.build()?;
builder.add_output_note(RawOutputNote::Full(trigger_note.clone()));
let mock_chain = builder.build()?;
let executed_transaction = mock_chain
.build_tx_context(faucet.id(), &[trigger_note.id()], &[])?
.add_note_script(output_note_script)
.with_source_manager(source_manager)
.build()?
.execute()
.await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 1);
assert_note_created!(
executed_transaction,
note_type: NoteType::Public,
sender: faucet.id(),
assets: [FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?],
);
let output_note = executed_transaction.output_notes().get_note(0);
let full_note = match output_note {
RawOutputNote::Full(note) => note,
_ => panic!("Expected OutputNote::Full variant"),
};
assert_eq!(
full_note.recipient().storage().commitment(),
note_storage.commitment(),
"Output note storage commitment should match expected storage commitment"
);
assert_eq!(
full_note.recipient().storage().num_items(),
note_storage.num_items(),
"Output note number of storage items should match expected number of storage items"
);
assert_eq!(full_note.id(), expected_note.id());
assert_eq!(
executed_transaction.account_patch().final_nonce(),
Some(faucet.nonce() + Felt::ONE)
);
Ok(())
}
#[tokio::test]
async fn network_faucet_mint() -> anyhow::Result<()> {
let max_supply = 1000u64;
let token_supply = 50u64;
let mut builder = MockChain::builder();
let faucet_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = builder.add_existing_network_faucet(
"NET",
max_supply,
faucet_owner_account_id,
Some(token_supply),
MintPolicy::owner_only(),
[],
)?;
let mut target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let actual_max_supply = FungibleFaucet::try_from(faucet.storage())?.max_supply();
assert_eq!(actual_max_supply.as_u64(), max_supply);
let stored_owner_id = faucet.storage().get_item(Ownable2Step::slot_name()).unwrap();
assert_eq!(
stored_owner_id[0],
Felt::new_unchecked(faucet_owner_account_id.suffix().as_canonical_u64())
);
assert_eq!(stored_owner_id[1], faucet_owner_account_id.prefix().as_felt());
assert_eq!(stored_owner_id[2], Felt::ZERO); assert_eq!(stored_owner_id[3], Felt::ZERO);
let initial_token_supply = FungibleFaucet::try_from(faucet.storage())?.token_supply();
assert_eq!(initial_token_supply.as_u64(), token_supply);
let amount = Felt::new_unchecked(75);
let mint_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap();
let serial_num = Word::default();
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_mint_output_note = Note::from(
P2idNote::builder()
.sender(faucet.id())
.target(target_account.id())
.assets(vec![mint_asset])
.note_type(NoteType::Private)
.serial_number(serial_num)
.build()
.unwrap(),
);
let recipient = p2id_mint_output_note.recipient().digest();
let mint_storage =
MintNoteStorage::new_fungible_private(recipient, mint_asset, output_note_tag);
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note: Note = MintNote::builder()
.sender(faucet_owner_account_id)
.mint_storage(mint_storage)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(mint_note.clone()));
let mut mock_chain = builder.build()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[mint_note.id()], &[])?.build()?;
let executed_transaction = tx_context.execute().await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 1);
let output_note = executed_transaction.output_notes().get_note(0);
let expected_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let assets = NoteAssets::new(vec![expected_asset.into()])?;
let details_commitment =
NoteDetailsCommitment::from_raw_commitments(recipient, assets.commitment());
let expected_note_id = NoteId::new(details_commitment, output_note.metadata());
assert_eq!(output_note.id(), expected_note_id);
assert_eq!(output_note.metadata().sender(), faucet.id());
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?;
let consume_tx_context = mock_chain
.build_tx_context(target_account.id(), &[], slice::from_ref(&p2id_mint_output_note))?
.foreign_accounts(vec![faucet_inputs])
.build()?;
let consume_executed_transaction = consume_tx_context.execute().await?;
target_account.apply_patch(consume_executed_transaction.account_patch())?;
let actual_asset = target_account.vault().get(expected_asset.id()).unwrap();
assert_eq!(actual_asset, Asset::from(expected_asset));
Ok(())
}
#[tokio::test]
async fn test_network_faucet_owner_can_mint() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
MintPolicy::owner_only(),
[],
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let mock_chain = builder.build()?;
let amount = Felt::new_unchecked(75);
let mint_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_note = Note::from(
P2idNote::builder()
.sender(faucet.id())
.target(target_account.id())
.assets(vec![mint_asset])
.note_type(NoteType::Private)
.serial_number(Word::default())
.build()?,
);
let recipient = p2id_note.recipient().digest();
let mint_inputs = MintNoteStorage::new_fungible_private(recipient, mint_asset, output_note_tag);
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note: Note = MintNote::builder()
.sender(owner_account_id)
.mint_storage(mint_inputs)
.generate_serial_number(&mut rng)
.build()?
.into();
let tx_context = mock_chain.build_tx_context(faucet.id(), &[], &[mint_note])?.build()?;
let executed_transaction = tx_context.execute().await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 1);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_set_policy_rejects_non_allowed_root() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let invalid_policy_root = FungibleFaucet::mint_and_send_root().as_word();
let set_policy_note_script = compile_note_script(&format!(
r#"
use miden::standards::faucets::policies::policy_manager
@note_script
pub proc main
repeat.12 push.0 end
push.{invalid_policy_root}
call.policy_manager::set_mint_policy
dropw dropw dropw dropw
end
"#
))?;
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(0),
MintPolicy::owner_only(),
[set_policy_note_script.root()],
)?;
let mock_chain = builder.build()?;
let result = execute_faucet_note_script(
&mock_chain,
faucet.id(),
owner_account_id,
set_policy_note_script,
400,
)
.await?;
assert_transaction_executor_error!(result, ERR_MINT_POLICY_ROOT_NOT_ALLOWED);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_set_burn_policy_rejects_non_allowed_root() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let invalid_policy_root = FungibleFaucet::receive_and_burn_root().as_word();
let set_policy_note_script =
compile_note_script(&create_set_burn_policy_note_script(invalid_policy_root))?;
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(0),
MintPolicy::owner_only(),
[set_policy_note_script.root()],
)?;
let mock_chain = builder.build()?;
let result = execute_faucet_note_script(
&mock_chain,
faucet.id(),
owner_account_id,
set_policy_note_script,
401,
)
.await?;
assert_transaction_executor_error!(result, ERR_BURN_POLICY_ROOT_NOT_ALLOWED);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_policy_getters_works() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let getters_note_script = compile_note_script(&create_policy_getters_note_script(
MintPolicy::allow_all().root(),
BurnPolicy::allow_all().root(),
TransferPolicy::allow_all().root(),
TransferPolicy::allow_all().root(),
))?;
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(0),
MintPolicy::allow_all(),
[getters_note_script.root()],
)?;
let mock_chain = builder.build()?;
let result = execute_faucet_note_script(
&mock_chain,
faucet.id(),
owner_account_id,
getters_note_script,
402,
)
.await?;
result.map_err(|err| anyhow::anyhow!("policy getter conformance tx failed: {err:?}"))?;
Ok(())
}
#[tokio::test]
async fn test_network_faucet_non_owner_cannot_mint() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let non_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32]);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
MintPolicy::owner_only(),
[],
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let mock_chain = builder.build()?;
let amount = Felt::new_unchecked(75);
let mint_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_note = Note::from(
P2idNote::builder()
.sender(faucet.id())
.target(target_account.id())
.assets(vec![mint_asset])
.note_type(NoteType::Private)
.serial_number(Word::default())
.build()?,
);
let recipient = p2id_note.recipient().digest();
let mint_inputs = MintNoteStorage::new_fungible_private(recipient, mint_asset, output_note_tag);
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note: Note = MintNote::builder()
.sender(non_owner_account_id)
.mint_storage(mint_inputs)
.generate_serial_number(&mut rng)
.build()?
.into();
let tx_context = mock_chain.build_tx_context(faucet.id(), &[], &[mint_note])?.build()?;
let result = tx_context.execute().await;
let expected_error = ERR_SENDER_NOT_OWNER;
assert_transaction_executor_error!(result, expected_error);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_owner_storage() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
MintPolicy::owner_only(),
[],
)?;
let _mock_chain = builder.build()?;
let stored_owner = faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(
stored_owner[0],
Felt::new_unchecked(owner_account_id.suffix().as_canonical_u64())
);
assert_eq!(stored_owner[1], owner_account_id.prefix().as_felt());
assert_eq!(stored_owner[2], Felt::ZERO); assert_eq!(stored_owner[3], Felt::ZERO);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let initial_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let new_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32]);
let transfer_note_script_code = format!(
r#"
use miden::standards::access::ownable2step
@note_script
pub proc main
repeat.14 push.0 end
push.{new_owner_prefix}
push.{new_owner_suffix}
call.ownable2step::transfer_ownership
dropw dropw dropw dropw
end
"#,
new_owner_prefix = new_owner_account_id.prefix().as_felt(),
new_owner_suffix = Felt::new_unchecked(new_owner_account_id.suffix().as_canonical_u64()),
);
let accept_note_script_code = r#"
use miden::standards::access::ownable2step
@note_script
pub proc main
repeat.16 push.0 end
call.ownable2step::accept_ownership
dropw dropw dropw dropw
end
"#;
let transfer_script = compile_note_script(&transfer_note_script_code)?;
let accept_script = compile_note_script(accept_note_script_code)?;
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
initial_owner_account_id,
Some(50),
MintPolicy::owner_only(),
[transfer_script.root(), accept_script.root()],
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let amount = Felt::new_unchecked(75);
let mint_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_note = Note::from(
P2idNote::builder()
.sender(faucet.id())
.target(target_account.id())
.assets(vec![mint_asset])
.note_type(NoteType::Private)
.serial_number(Word::default())
.build()?,
);
let recipient = p2id_note.recipient().digest();
let mint_inputs = MintNoteStorage::new_fungible_private(recipient, mint_asset, output_note_tag);
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note: Note = MintNote::builder()
.sender(initial_owner_account_id)
.mint_storage(mint_inputs.clone())
.generate_serial_number(&mut rng)
.build()?
.into();
let source_manager = Arc::new(DefaultSourceManager::default());
let mut rng = RandomCoin::new([Felt::from(200u32); 4].into());
let transfer_note = NoteBuilder::new(initial_owner_account_id, &mut rng)
.note_type(NoteType::Private)
.tag(NoteTag::default().into())
.serial_number(Word::from([11, 22, 33, 44u32]))
.script(transfer_script)
.build()?;
builder.add_output_note(RawOutputNote::Full(transfer_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[], &[mint_note])?.build()?;
let executed_transaction = tx_context.execute().await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 1);
let tx_context = mock_chain
.build_tx_context(faucet.id(), &[transfer_note.id()], &[])?
.with_source_manager(source_manager.clone())
.build()?;
let executed_transaction = tx_context.execute().await?;
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
let mut updated_faucet = faucet.clone();
updated_faucet.apply_patch(executed_transaction.account_patch())?;
let mut rng = RandomCoin::new([Felt::from(400u32); 4].into());
let accept_note = NoteBuilder::new(new_owner_account_id, &mut rng)
.note_type(NoteType::Private)
.tag(NoteTag::default().into())
.serial_number(Word::from([55, 66, 77, 88u32]))
.script(accept_script)
.build()?;
let tx_context = mock_chain
.build_tx_context(updated_faucet.clone(), &[], slice::from_ref(&accept_note))?
.with_source_manager(source_manager.clone())
.build()?;
let executed_transaction = tx_context.execute().await?;
let mut final_faucet = updated_faucet.clone();
final_faucet.apply_patch(executed_transaction.account_patch())?;
let stored_owner = final_faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(
stored_owner[0],
Felt::new_unchecked(new_owner_account_id.suffix().as_canonical_u64())
);
assert_eq!(stored_owner[1], new_owner_account_id.prefix().as_felt());
assert_eq!(stored_owner[2], Felt::ZERO); assert_eq!(stored_owner[3], Felt::ZERO);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_only_owner_can_transfer() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let non_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32]);
let new_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([3; 32]);
let transfer_note_script_code = format!(
r#"
use miden::standards::access::ownable2step
@note_script
pub proc main
repeat.14 push.0 end
push.{new_owner_prefix}
push.{new_owner_suffix}
call.ownable2step::transfer_ownership
dropw dropw dropw dropw
end
"#,
new_owner_prefix = new_owner_account_id.prefix().as_felt(),
new_owner_suffix = Felt::new_unchecked(new_owner_account_id.suffix().as_canonical_u64()),
);
let transfer_script = compile_note_script(&transfer_note_script_code)?;
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
MintPolicy::owner_only(),
[transfer_script.root()],
)?;
let mock_chain = builder.build()?;
let source_manager = Arc::new(DefaultSourceManager::default());
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let transfer_note = NoteBuilder::new(non_owner_account_id, &mut rng)
.note_type(NoteType::Private)
.tag(NoteTag::default().into())
.serial_number(Word::from([10, 20, 30, 40u32]))
.script(transfer_script)
.build()?;
let tx_context = mock_chain
.build_tx_context(faucet.id(), &[], &[transfer_note])?
.with_source_manager(source_manager.clone())
.build()?;
let result = tx_context.execute().await;
assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let new_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32]);
let renounce_note_script_code = r#"
use miden::standards::access::ownable2step
@note_script
pub proc main
repeat.16 push.0 end
call.ownable2step::renounce_ownership
dropw dropw dropw dropw
end
"#;
let transfer_note_script_code = format!(
r#"
use miden::standards::access::ownable2step
@note_script
pub proc main
repeat.14 push.0 end
push.{new_owner_prefix}
push.{new_owner_suffix}
call.ownable2step::transfer_ownership
dropw dropw dropw dropw
end
"#,
new_owner_prefix = new_owner_account_id.prefix().as_felt(),
new_owner_suffix = Felt::new_unchecked(new_owner_account_id.suffix().as_canonical_u64()),
);
let renounce_script = compile_note_script(renounce_note_script_code)?;
let transfer_script = compile_note_script(&transfer_note_script_code)?;
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
MintPolicy::owner_only(),
[renounce_script.root(), transfer_script.root()],
)?;
let stored_owner_before = faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(stored_owner_before[0], owner_account_id.suffix());
assert_eq!(stored_owner_before[1], owner_account_id.prefix().as_felt());
let source_manager = Arc::new(DefaultSourceManager::default());
let mut rng = RandomCoin::new([Felt::from(200u32); 4].into());
let renounce_note = NoteBuilder::new(owner_account_id, &mut rng)
.note_type(NoteType::Private)
.tag(NoteTag::default().into())
.serial_number(Word::from([11, 22, 33, 44u32]))
.script(renounce_script)
.build()?;
let mut rng = RandomCoin::new([Felt::from(300u32); 4].into());
let transfer_note = NoteBuilder::new(owner_account_id, &mut rng)
.note_type(NoteType::Private)
.tag(NoteTag::default().into())
.serial_number(Word::from([50, 60, 70, 80u32]))
.script(transfer_script)
.build()?;
builder.add_output_note(RawOutputNote::Full(renounce_note.clone()));
builder.add_output_note(RawOutputNote::Full(transfer_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let tx_context = mock_chain
.build_tx_context(faucet.id(), &[renounce_note.id()], &[])?
.with_source_manager(source_manager.clone())
.build()?;
let executed_transaction = tx_context.execute().await?;
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
let mut updated_faucet = faucet.clone();
updated_faucet.apply_patch(executed_transaction.account_patch())?;
let stored_owner_after = updated_faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(stored_owner_after[0], Felt::ZERO);
assert_eq!(stored_owner_after[1], Felt::ZERO);
assert_eq!(stored_owner_after[2], Felt::ZERO);
assert_eq!(stored_owner_after[3], Felt::ZERO);
mock_chain.prove_next_block()?;
let tx_context = mock_chain
.build_tx_context(updated_faucet.id(), &[transfer_note.id()], &[])?
.with_source_manager(source_manager.clone())
.build()?;
let result = tx_context.execute().await;
assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER);
Ok(())
}
#[test]
fn test_network_faucet_contains_default_burn_policy_root() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = builder.add_existing_network_faucet(
"NET",
200,
owner_account_id,
Some(100),
MintPolicy::owner_only(),
[],
)?;
let stored_root = faucet.storage().get_item(TokenPolicyManager::active_burn_policy_slot())?;
assert_eq!(stored_root, BurnAllowAll::root().as_word());
assert!(faucet.code().has_procedure(stored_root));
Ok(())
}
#[tokio::test]
async fn network_faucet_burn() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let mut faucet = builder.add_existing_network_faucet(
"NET",
200,
faucet_owner_account_id,
Some(100),
MintPolicy::owner_only(),
[],
)?;
let burn_amount = 100u64;
let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap();
let mut rng = RandomCoin::new([Felt::from(99u32); 4].into());
let note: Note = BurnNote::builder()
.sender(faucet_owner_account_id)
.asset(fungible_asset)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let initial_token_supply = FungibleFaucet::try_from(faucet.storage())?.token_supply();
assert_eq!(initial_token_supply, AssetAmount::from(100u32));
let tx_context = mock_chain.build_tx_context(faucet.id(), &[note.id()], &[])?.build()?;
let executed_transaction = tx_context.execute().await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 0);
assert_eq!(
executed_transaction.account_patch().final_nonce(),
Some(faucet.nonce() + Felt::ONE)
);
assert_eq!(executed_transaction.input_notes().get_note(0).id(), note.id());
faucet.apply_patch(executed_transaction.account_patch())?;
let final_token_supply = FungibleFaucet::try_from(faucet.storage())?.token_supply();
assert_eq!(
final_token_supply,
AssetAmount::new(initial_token_supply.as_u64() - burn_amount).unwrap()
);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_non_owner_cannot_burn_when_owner_only_policy_active()
-> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let non_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32]);
let faucet = build_network_faucet_with_burn_switching(
&mut builder,
"NET",
200,
owner_account_id,
100,
MintPolicy::owner_only(),
)?;
let set_policy_note_script =
create_set_burn_policy_note_script(BurnOwnerOnly::root().as_word());
let mut rng = RandomCoin::new([Felt::from(500u32); 4].into());
let set_policy_note = NoteBuilder::new(owner_account_id, &mut rng)
.note_type(NoteType::Private)
.code(set_policy_note_script.as_str())
.build()?;
let burn_amount = 10u64;
let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap();
let mut rng = RandomCoin::new([Felt::from(501u32); 4].into());
let burn_note: Note = BurnNote::builder()
.sender(non_owner_account_id)
.asset(fungible_asset)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(set_policy_note.clone()));
builder.add_output_note(RawOutputNote::Full(burn_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let source_manager = Arc::new(DefaultSourceManager::default());
let tx_context = mock_chain
.build_tx_context(faucet.id(), &[set_policy_note.id()], &[])?
.with_source_manager(source_manager.clone())
.build()?;
let executed_transaction = tx_context.execute().await?;
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?;
let result = tx_context.execute().await;
assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_owner_can_burn_when_owner_only_policy_active() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = build_network_faucet_with_burn_switching(
&mut builder,
"NET",
200,
owner_account_id,
100,
MintPolicy::owner_only(),
)?;
let set_policy_note_script =
create_set_burn_policy_note_script(BurnOwnerOnly::root().as_word());
let mut rng = RandomCoin::new([Felt::from(510u32); 4].into());
let set_policy_note = NoteBuilder::new(owner_account_id, &mut rng)
.note_type(NoteType::Private)
.code(set_policy_note_script.as_str())
.build()?;
let burn_amount = 10u64;
let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap();
let mut rng = RandomCoin::new([Felt::from(511u32); 4].into());
let burn_note: Note = BurnNote::builder()
.sender(owner_account_id)
.asset(fungible_asset)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(set_policy_note.clone()));
builder.add_output_note(RawOutputNote::Full(burn_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let source_manager = Arc::new(DefaultSourceManager::default());
let tx_context = mock_chain
.build_tx_context(faucet.id(), &[set_policy_note.id()], &[])?
.with_source_manager(source_manager.clone())
.build()?;
let executed_transaction = tx_context.execute().await?;
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?;
let executed_transaction = tx_context.execute().await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 0);
assert_eq!(
executed_transaction.account_patch().final_nonce(),
Some(faucet.nonce() + Felt::from(2u8),),
"nonce should be incremented by 1 in each of the 2 txs"
);
Ok(())
}
#[test]
fn test_network_faucet_min_burn_amount_policy_is_active() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = build_network_faucet_with_min_burn_amount(
&mut builder,
"NET",
200,
owner_account_id,
100,
50,
)?;
let stored_root = faucet.storage().get_item(TokenPolicyManager::active_burn_policy_slot())?;
assert_eq!(stored_root, MinBurnAmount::root().as_word());
assert!(faucet.code().has_procedure(stored_root));
Ok(())
}
#[tokio::test]
async fn test_network_faucet_burn_below_min_burn_amount_fails() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = build_network_faucet_with_min_burn_amount(
&mut builder,
"NET",
200,
owner_account_id,
100,
50,
)?;
let burn_amount = 10u64;
let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap();
let mut rng = RandomCoin::new([Felt::from(600u32); 4].into());
let burn_note: Note = BurnNote::builder()
.sender(owner_account_id)
.asset(fungible_asset)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(burn_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?;
let result = tx_context.execute().await;
assert_transaction_executor_error!(result, ERR_BURN_AMOUNT_BELOW_MIN_BURN_AMOUNT);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_burn_at_min_burn_amount_succeeds() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let mut faucet = build_network_faucet_with_min_burn_amount(
&mut builder,
"NET",
200,
owner_account_id,
100,
50,
)?;
let burn_amount = 50u64;
let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap();
let mut rng = RandomCoin::new([Felt::from(601u32); 4].into());
let burn_note: Note = BurnNote::builder()
.sender(owner_account_id)
.asset(fungible_asset)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(burn_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let initial_token_supply = FungibleFaucet::try_from(faucet.storage())?.token_supply();
let tx_context = mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?;
let executed_transaction = tx_context.execute().await?;
faucet.apply_patch(executed_transaction.account_patch())?;
let final_token_supply = FungibleFaucet::try_from(faucet.storage())?.token_supply();
assert_eq!(
final_token_supply,
AssetAmount::new(initial_token_supply.as_u64() - burn_amount).unwrap()
);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_owner_can_set_min_burn_amount() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let mut faucet = build_network_faucet_with_min_burn_amount(
&mut builder,
"NET",
200,
owner_account_id,
100,
50,
)?;
let set_note_script = create_set_min_burn_amount_note_script(5);
let mut rng = RandomCoin::new([Felt::from(610u32); 4].into());
let set_note = NoteBuilder::new(owner_account_id, &mut rng)
.note_type(NoteType::Private)
.code(set_note_script.as_str())
.build()?;
let burn_amount = 10u64;
let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap();
let mut rng = RandomCoin::new([Felt::from(611u32); 4].into());
let burn_note: Note = BurnNote::builder()
.sender(owner_account_id)
.asset(fungible_asset)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(set_note.clone()));
builder.add_output_note(RawOutputNote::Full(burn_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let initial_token_supply = FungibleFaucet::try_from(faucet.storage())?.token_supply();
let source_manager = Arc::new(DefaultSourceManager::default());
let tx_context = mock_chain
.build_tx_context(faucet.id(), &[set_note.id()], &[])?
.with_source_manager(source_manager.clone())
.build()?;
let set_transaction = tx_context.execute().await?;
mock_chain.add_pending_executed_transaction(&set_transaction)?;
mock_chain.prove_next_block()?;
faucet.apply_patch(set_transaction.account_patch())?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?;
let burn_transaction = tx_context.execute().await?;
faucet.apply_patch(burn_transaction.account_patch())?;
let final_token_supply = FungibleFaucet::try_from(faucet.storage())?.token_supply();
assert_eq!(
final_token_supply,
AssetAmount::new(initial_token_supply.as_u64() - burn_amount).unwrap()
);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_non_owner_cannot_set_min_burn_amount() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let non_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32]);
let faucet = build_network_faucet_with_min_burn_amount(
&mut builder,
"NET",
200,
owner_account_id,
100,
50,
)?;
let set_note_script = create_set_min_burn_amount_note_script(5);
let mut rng = RandomCoin::new([Felt::from(620u32); 4].into());
let set_note = NoteBuilder::new(non_owner_account_id, &mut rng)
.note_type(NoteType::Private)
.code(set_note_script.as_str())
.build()?;
builder.add_output_note(RawOutputNote::Full(set_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[set_note.id()], &[])?.build()?;
let result = tx_context.execute().await;
assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER);
Ok(())
}
fn build_network_faucet_mutable_max_supply(
builder: &mut MockChainBuilder,
token_symbol: &str,
max_supply: u64,
owner: AccountId,
) -> anyhow::Result<Account> {
let name = TokenName::new(token_symbol)?;
let symbol = TokenSymbol::new(token_symbol)?;
let max_supply = AssetAmount::new(max_supply)?;
let faucet = FungibleFaucet::builder()
.name(name)
.symbol(symbol)
.decimals(10)
.max_supply(max_supply)
.is_max_supply_mutable(true)
.build()?;
let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::allow_all())
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();
let account_builder = AccountBuilder::new(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_component(faucet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_asset_callbacks(AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()))
.with_components(token_policy_manager)
.with_component(Pausable::unpaused());
builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
}
fn create_set_max_supply_note_script(new_max_supply: u64) -> NoteScript {
let code = format!(
r#"
@note_script
pub proc main
padw padw padw push.0.0.0
push.{new_max_supply}
call.::miden::standards::faucets::fungible::set_max_supply
dropw dropw dropw dropw
end
"#
);
CodeBuilder::default().compile_note_script(&code).unwrap()
}
#[tokio::test]
async fn test_set_max_supply_rejects_cap_above_fungible_asset_max_amount() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet =
build_network_faucet_mutable_max_supply(&mut builder, "NET", 200, owner_account_id)?;
let new_max_supply = FungibleAsset::MAX_AMOUNT.as_u64() + 1;
let set_note_script = create_set_max_supply_note_script(new_max_supply);
let mut rng = RandomCoin::new([Felt::from(630u32); 4].into());
let set_note = NoteBuilder::new(owner_account_id, &mut rng)
.note_type(NoteType::Private)
.script(set_note_script)
.build()?;
builder.add_output_note(RawOutputNote::Full(set_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[set_note.id()], &[])?.build()?;
let result = tx_context.execute().await;
assert_transaction_executor_error!(
result,
ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT
);
Ok(())
}
#[rstest::rstest]
#[case::private(NoteType::Private)]
#[case::public(NoteType::Public)]
#[tokio::test]
async fn test_mint_note_output_note_types(#[case] note_type: NoteType) -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
faucet_owner_account_id,
Some(50),
MintPolicy::owner_only(),
[],
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let amount = Felt::new_unchecked(75);
let mint_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap();
let serial_num = Word::from([1, 2, 3, 4u32]);
let p2id_mint_output_note = Note::from(
P2idNote::builder()
.sender(faucet.id())
.target(target_account.id())
.assets(vec![mint_asset])
.note_type(note_type)
.serial_number(serial_num)
.build()
.unwrap(),
);
let mint_storage = match note_type {
NoteType::Private => {
let output_note_tag = NoteTag::with_account_target(target_account.id());
let recipient = p2id_mint_output_note.recipient().digest();
MintNoteStorage::new_fungible_private(recipient, mint_asset, output_note_tag)
},
NoteType::Public => {
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_script = StandardNote::P2ID.script();
let p2id_storage =
vec![target_account.id().suffix(), target_account.id().prefix().as_felt()];
let note_storage = NoteStorage::new(p2id_storage)?;
let recipient = NoteRecipient::new(serial_num, p2id_script, note_storage);
MintNoteStorage::new_fungible_public(recipient, mint_asset, output_note_tag)?
},
};
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note: Note = MintNote::builder()
.sender(faucet_owner_account_id)
.mint_storage(mint_storage.clone())
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(mint_note.clone()));
let mut mock_chain = builder.build()?;
let tx_context = mock_chain.build_tx_context(faucet.id(), &[mint_note.id()], &[])?.build()?;
let executed_transaction = tx_context.execute().await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 1);
let output_note = executed_transaction.output_notes().get_note(0);
match note_type {
NoteType::Private => {
assert_eq!(output_note.id(), p2id_mint_output_note.id());
assert_eq!(output_note.metadata(), p2id_mint_output_note.metadata());
},
NoteType::Public => {
let created_note = match output_note {
RawOutputNote::Full(note) => note,
_ => panic!("Expected OutputNote::Full variant"),
};
assert_eq!(created_note, &p2id_mint_output_note);
},
}
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
let mut target_account_mut = target_account.clone();
let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?;
let consume_tx_context = mock_chain
.build_tx_context(target_account.id(), &[], slice::from_ref(&p2id_mint_output_note))?
.foreign_accounts(vec![faucet_inputs])
.build()?;
let consume_executed_transaction = consume_tx_context.execute().await?;
target_account_mut.apply_patch(consume_executed_transaction.account_patch())?;
let expected_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let balance = target_account_mut.vault().get_balance(expected_asset.id())?;
assert_eq!(balance, expected_asset.amount());
Ok(())
}
#[tokio::test]
async fn multiple_mints_in_single_tx_produce_correct_amounts() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet = builder.add_existing_basic_faucet(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
"TST",
300,
None,
)?;
let mock_chain = builder.build()?;
let recipient_1 = Word::from([0, 1, 2, 3u32]);
let recipient_2 = Word::from([4, 5, 6, 7u32]);
let tag = NoteTag::default();
let note_type = NoteType::Private;
let amount_1: u64 = 100;
let amount_2: u64 = 50;
let tx_script_code = format!(
"
@transaction_script
pub proc main
# --- First mint: mint {amount_1} tokens to recipient_1 ---
push.{recipient_1}
push.{note_type}
push.{tag}
push.{amount_1}
push.{faucet_id_prefix}
push.{faucet_id_suffix}
# => [faucet_id_suffix, faucet_id_prefix, amount_1, tag, note_type, RECIPIENT_1]
exec.::miden::standards::assets::fungible_asset::create
# => [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT_1]
call.::miden::standards::faucets::fungible::mint_and_send
# => [note_idx, pad(15)]
# clean up the stack before the second call
dropw dropw dropw dropw
# --- Second mint: mint {amount_2} tokens to recipient_2 ---
push.{recipient_2}
push.{note_type}
push.{tag}
push.{amount_2}
push.{faucet_id_prefix}
push.{faucet_id_suffix}
# => [faucet_id_suffix, faucet_id_prefix, amount_2, tag, note_type, RECIPIENT_2]
exec.::miden::standards::assets::fungible_asset::create
# => [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT_2]
call.::miden::standards::faucets::fungible::mint_and_send
# => [note_idx, pad(15)]
# truncate the stack
dropw dropw dropw dropw
end
",
note_type = note_type as u8,
tag = u32::from(tag),
faucet_id_suffix = faucet.id().suffix(),
faucet_id_prefix = faucet.id().prefix().as_felt(),
);
let source_manager = Arc::new(DefaultSourceManager::default());
let tx_script = CodeBuilder::with_source_manager(source_manager.clone())
.compile_tx_script(tx_script_code)?;
let tx_context = mock_chain
.build_tx_context(faucet.clone(), &[], &[])?
.tx_script(tx_script)
.with_source_manager(source_manager)
.build()?;
let executed_transaction = tx_context.execute().await?;
assert_eq!(executed_transaction.output_notes().num_notes(), 2);
let expected_asset_1: Asset = FungibleAsset::new(faucet.id(), amount_1)?.into();
let output_note_1 = executed_transaction.output_notes().get_note(0);
let assets_1 = NoteAssets::new(vec![expected_asset_1])?;
let details_commitment_1 =
NoteDetailsCommitment::from_raw_commitments(recipient_1, assets_1.commitment());
let expected_id_1 = NoteId::new(details_commitment_1, output_note_1.metadata());
assert_eq!(output_note_1.id(), expected_id_1);
let expected_asset_2: Asset = FungibleAsset::new(faucet.id(), amount_2)?.into();
let output_note_2 = executed_transaction.output_notes().get_note(1);
let assets_2 = NoteAssets::new(vec![expected_asset_2])?;
let details_commitment_2 =
NoteDetailsCommitment::from_raw_commitments(recipient_2, assets_2.commitment());
let expected_id_2 = NoteId::new(details_commitment_2, output_note_2.metadata());
assert_eq!(output_note_2.id(), expected_id_2);
Ok(())
}
fn build_network_faucet_with_blocklist_transfer(
builder: &mut MockChainBuilder,
token_symbol: &str,
max_supply: u64,
owner: AccountId,
token_supply: u64,
) -> anyhow::Result<Account> {
let name = TokenName::new(token_symbol)?;
let symbol = TokenSymbol::new(token_symbol)?;
let max_supply = AssetAmount::new(max_supply)?;
let token_supply = AssetAmount::new(token_supply)?;
let faucet = FungibleFaucet::builder()
.name(name)
.symbol(symbol)
.decimals(10)
.max_supply(max_supply)
.token_supply(token_supply)
.build()?;
let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::allow_all())
.active_send_policy(TransferPolicy::empty_basic_blocklist())
.active_receive_policy(TransferPolicy::empty_basic_blocklist())
.build();
let account_builder = AccountBuilder::new(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_component(faucet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_asset_callbacks(AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()))
.with_components(token_policy_manager)
.with_component(Pausable::unpaused());
builder.add_account_from_builder(
Auth::NetworkAccount {
allowed_script_roots: BTreeSet::from([MintNote::script_root()]),
allowed_tx_script_roots: BTreeSet::new(),
},
account_builder,
AccountState::Exists,
)
}
#[tokio::test]
async fn network_faucet_mint_with_blocklist() -> anyhow::Result<()> {
let max_supply = 1000u64;
let token_supply = 50u64;
let mut builder = MockChain::builder();
let faucet_owner_account_id =
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
let faucet = build_network_faucet_with_blocklist_transfer(
&mut builder,
"NET",
max_supply,
faucet_owner_account_id,
token_supply,
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let amount = Felt::new_unchecked(75);
let mint_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap();
let serial_num = Word::default();
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_mint_output_note = Note::from(
P2idNote::builder()
.sender(faucet.id())
.target(target_account.id())
.assets(vec![mint_asset])
.note_type(NoteType::Private)
.serial_number(serial_num)
.build()
.unwrap(),
);
let recipient = p2id_mint_output_note.recipient().digest();
let mint_storage =
MintNoteStorage::new_fungible_private(recipient, mint_asset, output_note_tag);
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note: Note = MintNote::builder()
.sender(faucet_owner_account_id)
.mint_storage(mint_storage)
.generate_serial_number(&mut rng)
.build()?
.into();
builder.add_output_note(RawOutputNote::Full(mint_note.clone()));
let mock_chain = builder.build()?;
let executed = mock_chain
.build_tx_context(faucet.id(), &[mint_note.id()], &[])?
.build()?
.execute()
.await?;
assert_eq!(executed.output_notes().num_notes(), 1);
Ok(())
}