use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::Path;
use miden_protocol::asset::AssetAmount;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteAttachments,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::StandardsLib;
use crate::account::faucets::{Description, ExternalLink, LogoURI};
use crate::note::NetworkAccountTarget;
use crate::note::costs::{FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
const FAUCET_METADATA_CONFIG_SCRIPT_PATH: &str =
"::miden::standards::notes::faucet_metadata_config::main";
static FAUCET_METADATA_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(FAUCET_METADATA_CONFIG_SCRIPT_PATH);
NoteScript::from_package_reference(standards_lib.as_ref(), path)
.expect("Standards library contains FAUCET_METADATA_CONFIG note script procedure")
});
const STRING_NUM_ELEMENTS: usize = 28;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FaucetMetadataConfig {
SetMaxSupply { max_supply: AssetAmount },
SetDescription { description: Description },
SetLogoUri { logo_uri: LogoURI },
SetExternalLink { external_link: ExternalLink },
}
impl FaucetMetadataConfig {
const SELECTOR_SET_MAX_SUPPLY: u8 = 0;
const SELECTOR_SET_DESCRIPTION: u8 = 1;
const SELECTOR_SET_LOGO_URI: u8 = 2;
const SELECTOR_SET_EXTERNAL_LINK: u8 = 3;
const fn selector(&self) -> u8 {
match self {
FaucetMetadataConfig::SetMaxSupply { .. } => Self::SELECTOR_SET_MAX_SUPPLY,
FaucetMetadataConfig::SetDescription { .. } => Self::SELECTOR_SET_DESCRIPTION,
FaucetMetadataConfig::SetLogoUri { .. } => Self::SELECTOR_SET_LOGO_URI,
FaucetMetadataConfig::SetExternalLink { .. } => Self::SELECTOR_SET_EXTERNAL_LINK,
}
}
fn to_storage_values(&self) -> Vec<Felt> {
let selector = Felt::from(self.selector());
match self {
FaucetMetadataConfig::SetMaxSupply { max_supply } => {
vec![selector, Felt::from(*max_supply)]
},
FaucetMetadataConfig::SetDescription { description } => {
string_storage_values(selector, &description.to_words())
},
FaucetMetadataConfig::SetLogoUri { logo_uri } => {
string_storage_values(selector, &logo_uri.to_words())
},
FaucetMetadataConfig::SetExternalLink { external_link } => {
string_storage_values(selector, &external_link.to_words())
},
}
}
}
fn string_storage_values(selector: Felt, value: &[Word]) -> Vec<Felt> {
let mut items = Vec::with_capacity(FaucetMetadataConfigNote::MAX_NUM_STORAGE_ITEMS);
items.push(selector);
items.extend([Felt::ZERO; 3]);
items.extend(value.iter().flat_map(Word::as_elements).copied());
debug_assert_eq!(items.len(), 4 + STRING_NUM_ELEMENTS);
items
}
impl From<FaucetMetadataConfig> for NoteStorage {
fn from(config: FaucetMetadataConfig) -> Self {
NoteStorage::new(config.to_storage_values())
.expect("number of storage items should not exceed max storage items")
}
}
#[derive(Debug, Clone)]
pub struct FaucetMetadataConfigNote {
sender: AccountId,
target: AccountId,
config: FaucetMetadataConfig,
serial_number: Word,
attachments: NoteAttachments,
}
#[bon::bon]
impl FaucetMetadataConfigNote {
#[builder]
pub fn new(
#[builder(field)] mut attachments: Vec<NoteAttachment>,
sender: AccountId,
target: AccountId,
config: FaucetMetadataConfig,
serial_number: Word,
) -> Result<Self, NoteError> {
NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
NoteError::other_with_source(
"failed to bind the FaucetMetadataConfig note to its target account",
err,
)
})?;
let attachments = NoteAttachments::new(attachments)?;
Ok(Self {
sender,
target,
config,
serial_number,
attachments,
})
}
}
impl FaucetMetadataConfigNote {
pub const MAX_NUM_STORAGE_ITEMS: usize = 4 + STRING_NUM_ELEMENTS;
pub fn script() -> NoteScript {
FAUCET_METADATA_CONFIG_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
FAUCET_METADATA_CONFIG_SCRIPT.root()
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn target(&self) -> AccountId {
self.target
}
pub fn config(&self) -> &FaucetMetadataConfig {
&self.config
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn attachments(&self) -> &NoteAttachments {
&self.attachments
}
}
impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S> {
pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
self.attachments.push(attachment.into());
self
}
pub fn attachments(
mut self,
attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
) -> Self {
self.attachments.extend(attachments.into_iter().map(Into::into));
self
}
}
impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S>
where
S::SerialNumber: faucet_metadata_config_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> FaucetMetadataConfigNoteBuilder<faucet_metadata_config_note_builder::SetSerialNumber<S>>
{
self.serial_number(rng.draw_word())
}
}
impl From<FaucetMetadataConfigNote> for Note {
fn from(note: FaucetMetadataConfigNote) -> Self {
let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
.with_tag(NoteTag::with_account_target(note.target));
let recipient = NoteRecipient::new(
note.serial_number,
FaucetMetadataConfigNote::script(),
NoteStorage::from(note.config),
);
Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
}
}
impl NoteConsumptionCost for FaucetMetadataConfigNote {
fn consumption_cycles() -> u32 {
FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::AccountType;
use miden_protocol::crypto::rand::RandomCoin;
use super::*;
fn account_id(seed: u8) -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([seed; 32])
}
fn description() -> Description {
Description::new("A described token").expect("description should be valid")
}
#[test]
fn builder_builds_faucet_metadata_config_note() {
let mut rng = RandomCoin::new(Word::empty());
let faucet = account_id(1);
let owner = account_id(2);
let note = FaucetMetadataConfigNote::builder()
.sender(owner)
.target(faucet)
.config(FaucetMetadataConfig::SetDescription { description: description() })
.generate_serial_number(&mut rng)
.build()
.unwrap();
assert_eq!(note.sender(), owner);
assert_eq!(note.target(), faucet);
let note = Note::from(note);
assert_eq!(note.metadata().note_type(), NoteType::Public);
assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet));
assert_eq!(note.assets().num_assets(), 0);
}
#[test]
fn set_max_supply_storage_layout() {
let max_supply = AssetAmount::new(1_000).unwrap();
let storage = NoteStorage::from(FaucetMetadataConfig::SetMaxSupply { max_supply });
assert_eq!(
storage.items(),
&[
Felt::from(FaucetMetadataConfig::SELECTOR_SET_MAX_SUPPLY),
Felt::from(max_supply),
]
);
}
#[test]
fn set_description_storage_layout() {
let description = description();
let storage = NoteStorage::from(FaucetMetadataConfig::SetDescription {
description: description.clone(),
});
let items = storage.items();
assert_eq!(items.len(), FaucetMetadataConfigNote::MAX_NUM_STORAGE_ITEMS);
assert_eq!(items[0], Felt::from(FaucetMetadataConfig::SELECTOR_SET_DESCRIPTION));
assert_eq!(&items[1..4], &[Felt::ZERO; 3]);
let payload: Vec<Felt> =
description.to_words().iter().flat_map(Word::as_elements).copied().collect();
assert_eq!(&items[4..], payload.as_slice());
}
#[test]
fn string_action_selectors() {
let logo_uri = LogoURI::new("https://example.com/logo.png").unwrap();
let storage = NoteStorage::from(FaucetMetadataConfig::SetLogoUri { logo_uri });
assert_eq!(storage.items()[0], Felt::from(FaucetMetadataConfig::SELECTOR_SET_LOGO_URI));
let external_link = ExternalLink::new("https://example.com").unwrap();
let storage = NoteStorage::from(FaucetMetadataConfig::SetExternalLink { external_link });
assert_eq!(
storage.items()[0],
Felt::from(FaucetMetadataConfig::SELECTOR_SET_EXTERNAL_LINK)
);
}
}