extern crate alloc;
use alloc::sync::Arc;
use core::slice;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{
Account,
AccountId,
AccountIdVersion,
AccountStorageMode,
AccountType,
};
use miden_protocol::assembly::DefaultSourceManager;
use miden_protocol::asset::{Asset, FungibleAsset};
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteId,
NoteMetadata,
NoteRecipient,
NoteStorage,
NoteTag,
NoteType,
};
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::Ownable2Step;
use miden_standards::account::faucets::{
BasicFungibleFaucet,
NetworkFungibleFaucet,
TokenMetadata,
};
use miden_standards::account::mint_policies::OwnerControlledInitConfig;
use miden_standards::code_builder::CodeBuilder;
use miden_standards::errors::standards::{
ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY,
ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY,
ERR_MINT_POLICY_ROOT_NOT_ALLOWED,
ERR_SENDER_NOT_OWNER,
};
use miden_standards::note::{BurnNote, MintNote, MintNoteStorage, StandardNote};
use miden_standards::testing::note::NoteBuilder;
use miden_testing::utils::create_p2id_note_exact;
use miden_testing::{Auth, MockChain, assert_transaction_executor_error};
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) -> String {
format!(
"
begin
# pad the stack before call
padw padw push.0
push.{recipient}
push.{note_type}
push.{tag}
push.{amount}
# => [amount, tag, note_type, RECIPIENT, pad(9)]
call.::miden::standards::faucets::basic_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,
)
}
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);
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 fungible_asset: Asset =
FungibleAsset::new(faucet.id(), params.amount.as_canonical_u64())?.into();
let output_note = executed_transaction.output_notes().get_note(0).clone();
let assets = NoteAssets::new(vec![fungible_asset])?;
let id = NoteId::new(params.recipient, assets.commitment());
assert_eq!(output_note.id(), id);
assert_eq!(
output_note.metadata(),
&NoteMetadata::new(faucet.id(), params.note_type).with_tag(params.tag)
);
Ok(())
}
async fn execute_faucet_note_script(
mock_chain: &MockChain,
faucet_id: AccountId,
sender_account_id: AccountId,
note_script_code: &str,
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)
.code(note_script_code)
.build()?;
let tx_context = mock_chain
.build_tx_context(faucet_id, &[], &[note])?
.with_source_manager(source_manager)
.build()?;
Ok(tx_context.execute().await)
}
#[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(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(4);
let amount = Felt::new(250);
let tx_script_code = format!(
"
begin
# pad the stack before call
padw padw push.0
push.{recipient}
push.{note_type}
push.{tag}
push.{amount}
# => [amount, tag, note_type, RECIPIENT, pad(9)]
call.::miden::standards::faucets::basic_fungible::mint_and_send
# => [note_idx, pad(15)]
# truncate the stack
dropw dropw dropw dropw
end
",
note_type = NoteType::Private as u8,
recipient = recipient,
);
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(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::basic_fungible::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 = TokenMetadata::try_from(faucet.storage())?;
assert_eq!(token_metadata.max_supply(), Felt::from(max_supply));
assert_eq!(token_metadata.token_supply(), Felt::from(token_supply));
let executed_transaction = mock_chain
.build_tx_context(faucet.id(), &[note.id()], &[])?
.build()?
.execute()
.await?;
prove_and_verify_transaction(executed_transaction.clone()).await?;
assert_eq!(executed_transaction.account_delta().nonce_delta(), Felt::new(1));
assert_eq!(executed_transaction.input_notes().get_note(0).id(), note.id());
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::basic_fungible::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(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::new(0),
Felt::new(0),
Felt::new(0),
Felt::new(1),
Felt::new(0),
])?;
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 = NoteMetadata::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::build_recipient
# => [RECIPIENT]
# Now call mint with the computed recipient
push.{note_type}
push.{tag}
push.{amount}
# => [amount, tag, note_type, RECIPIENT]
call.::miden::standards::faucets::basic_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,
);
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);
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.metadata().note_type(), NoteType::Public);
let expected_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let expected_asset_obj = Asset::from(expected_asset);
assert!(full_note.assets().iter().any(|asset| asset == &expected_asset_obj));
assert_eq!(full_note.metadata().sender(), faucet.id());
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_delta().nonce_delta(), Felt::new(1));
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::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
max_supply,
faucet_owner_account_id,
Some(token_supply),
OwnerControlledInitConfig::OwnerOnly,
)?;
let mut target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let actual_max_supply = TokenMetadata::try_from(faucet.storage())?.max_supply();
assert_eq!(actual_max_supply.as_canonical_u64(), max_supply);
let stored_owner_id = faucet.storage().get_item(Ownable2Step::slot_name()).unwrap();
assert_eq!(
stored_owner_id[0],
Felt::new(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::new(0)); assert_eq!(stored_owner_id[3], Felt::new(0));
let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply();
assert_eq!(initial_token_supply.as_canonical_u64(), token_supply);
let amount = Felt::new(75);
let mint_asset: Asset =
FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap().into();
let serial_num = Word::default();
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_mint_output_note = create_p2id_note_exact(
faucet.id(),
target_account.id(),
vec![mint_asset],
NoteType::Private,
serial_num,
)
.unwrap();
let recipient = p2id_mint_output_note.recipient().digest();
let mint_storage = MintNoteStorage::new_private(recipient, amount, output_note_tag.into());
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note = MintNote::create(
faucet.id(),
faucet_owner_account_id,
mint_storage,
NoteAttachment::default(),
&mut rng,
)?;
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 expected_note_id = NoteId::new(recipient, assets.commitment());
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 consume_tx_context = mock_chain
.build_tx_context(target_account.id(), &[], slice::from_ref(&p2id_mint_output_note))?
.build()?;
let consume_executed_transaction = consume_tx_context.execute().await?;
target_account.apply_delta(consume_executed_transaction.account_delta())?;
let balance = target_account.vault().get_balance(faucet.id())?;
assert_eq!(balance, expected_asset.amount(),);
Ok(())
}
#[tokio::test]
async fn test_network_faucet_owner_can_mint() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id = AccountId::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
OwnerControlledInitConfig::OwnerOnly,
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let mock_chain = builder.build()?;
let amount = Felt::new(75);
let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?.into();
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_note = create_p2id_note_exact(
faucet.id(),
target_account.id(),
vec![mint_asset],
NoteType::Private,
Word::default(),
)?;
let recipient = p2id_note.recipient().digest();
let mint_inputs = MintNoteStorage::new_private(recipient, amount, output_note_tag.into());
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note = MintNote::create(
faucet.id(),
owner_account_id,
mint_inputs,
NoteAttachment::default(),
&mut rng,
)?;
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::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(0),
OwnerControlledInitConfig::OwnerOnly,
)?;
let mock_chain = builder.build()?;
let invalid_policy_root = NetworkFungibleFaucet::mint_and_send_digest();
let set_policy_note_script = format!(
r#"
use miden::standards::mint_policies::policy_manager->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 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_non_owner_cannot_mint() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id = AccountId::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let non_owner_account_id = AccountId::dummy(
[2; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
OwnerControlledInitConfig::OwnerOnly,
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let mock_chain = builder.build()?;
let amount = Felt::new(75);
let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?.into();
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_note = create_p2id_note_exact(
faucet.id(),
target_account.id(),
vec![mint_asset],
NoteType::Private,
Word::default(),
)?;
let recipient = p2id_note.recipient().digest();
let mint_inputs = MintNoteStorage::new_private(recipient, amount, output_note_tag.into());
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note = MintNote::create(
faucet.id(),
non_owner_account_id,
mint_inputs,
NoteAttachment::default(),
&mut rng,
)?;
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::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
OwnerControlledInitConfig::OwnerOnly,
)?;
let _mock_chain = builder.build()?;
let stored_owner = faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(stored_owner[0], Felt::new(owner_account_id.suffix().as_canonical_u64()));
assert_eq!(stored_owner[1], owner_account_id.prefix().as_felt());
assert_eq!(stored_owner[2], Felt::new(0)); assert_eq!(stored_owner[3], Felt::new(0));
Ok(())
}
#[tokio::test]
async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let initial_owner_account_id = AccountId::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let new_owner_account_id = AccountId::dummy(
[2; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
initial_owner_account_id,
Some(50),
OwnerControlledInitConfig::OwnerOnly,
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let amount = Felt::new(75);
let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?.into();
let output_note_tag = NoteTag::with_account_target(target_account.id());
let p2id_note = create_p2id_note_exact(
faucet.id(),
target_account.id(),
vec![mint_asset],
NoteType::Private,
Word::default(),
)?;
let recipient = p2id_note.recipient().digest();
let mint_inputs = MintNoteStorage::new_private(recipient, amount, output_note_tag.into());
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note = MintNote::create(
faucet.id(),
initial_owner_account_id,
mint_inputs.clone(),
NoteAttachment::default(),
&mut rng,
)?;
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(new_owner_account_id.suffix().as_canonical_u64()),
);
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]))
.code(transfer_note_script_code.clone())
.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_delta(executed_transaction.account_delta())?;
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 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]))
.code(accept_note_script_code)
.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_delta(executed_transaction.account_delta())?;
let stored_owner = final_faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(stored_owner[0], Felt::new(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::new(0)); assert_eq!(stored_owner[3], Felt::new(0));
Ok(())
}
#[tokio::test]
async fn test_network_faucet_only_owner_can_transfer() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let owner_account_id = AccountId::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let non_owner_account_id = AccountId::dummy(
[2; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let new_owner_account_id = AccountId::dummy(
[3; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
OwnerControlledInitConfig::OwnerOnly,
)?;
let mock_chain = builder.build()?;
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(new_owner_account_id.suffix().as_canonical_u64()),
);
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]))
.code(transfer_note_script_code.clone())
.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::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let new_owner_account_id = AccountId::dummy(
[2; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
owner_account_id,
Some(50),
OwnerControlledInitConfig::OwnerOnly,
)?;
let stored_owner_before = faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(stored_owner_before[0], Felt::new(owner_account_id.suffix().as_canonical_u64()));
assert_eq!(stored_owner_before[1], owner_account_id.prefix().as_felt());
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 source_manager = Arc::new(DefaultSourceManager::default());
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(new_owner_account_id.suffix().as_canonical_u64()),
);
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]))
.code(renounce_note_script_code)
.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]))
.code(transfer_note_script_code.clone())
.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_delta(executed_transaction.account_delta())?;
let stored_owner_after = updated_faucet.storage().get_item(Ownable2Step::slot_name())?;
assert_eq!(stored_owner_after[0], Felt::new(0));
assert_eq!(stored_owner_after[1], Felt::new(0));
assert_eq!(stored_owner_after[2], Felt::new(0));
assert_eq!(stored_owner_after[3], Felt::new(0));
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_faucet_burn_procedures_are_identical() {
assert_eq!(
BasicFungibleFaucet::burn_digest(),
NetworkFungibleFaucet::burn_digest(),
"Basic and network fungible faucets must have the same burn procedure digest"
);
}
#[tokio::test]
async fn network_faucet_burn() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet_owner_account_id = AccountId::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let mut faucet = builder.add_existing_network_faucet(
"NET",
200,
faucet_owner_account_id,
Some(100),
OwnerControlledInitConfig::OwnerOnly,
)?;
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 = BurnNote::create(
faucet_owner_account_id,
faucet.id(),
fungible_asset.into(),
NoteAttachment::default(),
&mut rng,
)?;
builder.add_output_note(RawOutputNote::Full(note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply();
assert_eq!(initial_token_supply, Felt::new(100));
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_delta().nonce_delta(), Felt::new(1));
assert_eq!(executed_transaction.input_notes().get_note(0).id(), note.id());
faucet.apply_delta(executed_transaction.account_delta())?;
let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply();
assert_eq!(
final_token_supply,
Felt::new(initial_token_supply.as_canonical_u64() - burn_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::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::RegularAccountImmutableCode,
AccountStorageMode::Private,
);
let faucet = builder.add_existing_network_faucet(
"NET",
1000,
faucet_owner_account_id,
Some(50),
OwnerControlledInitConfig::OwnerOnly,
)?;
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let amount = Felt::new(75);
let mint_asset: Asset =
FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap().into();
let serial_num = Word::from([1, 2, 3, 4u32]);
let p2id_mint_output_note = create_p2id_note_exact(
faucet.id(),
target_account.id(),
vec![mint_asset],
note_type,
serial_num,
)
.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_private(recipient, amount, output_note_tag.into())
},
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_public(recipient, amount, output_note_tag.into())?
},
};
let mut rng = RandomCoin::new([Felt::from(42u32); 4].into());
let mint_note = MintNote::create(
faucet.id(),
faucet_owner_account_id,
mint_storage.clone(),
NoteAttachment::default(),
&mut rng,
)?;
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 consume_tx_context = mock_chain
.build_tx_context(target_account.id(), &[], slice::from_ref(&p2id_mint_output_note))?
.build()?;
let consume_executed_transaction = consume_tx_context.execute().await?;
target_account_mut.apply_delta(consume_executed_transaction.account_delta())?;
let expected_asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64())?;
let balance = target_account_mut.vault().get_balance(faucet.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!(
"
begin
# --- First mint: mint {amount_1} tokens to recipient_1 ---
padw padw push.0
push.{recipient_1}
push.{note_type}
push.{tag}
push.{amount_1}
# => [amount_1, tag, note_type, RECIPIENT_1, pad(9)]
call.::miden::standards::faucets::basic_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 ---
padw padw push.0
push.{recipient_2}
push.{note_type}
push.{tag}
push.{amount_2}
# => [amount_2, tag, note_type, RECIPIENT_2, pad(9)]
call.::miden::standards::faucets::basic_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),
);
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 expected_id_1 = NoteId::new(recipient_1, assets_1.commitment());
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 expected_id_2 = NoteId::new(recipient_2, assets_2.commitment());
assert_eq!(output_note_2.id(), expected_id_2);
Ok(())
}