use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::str::FromStr;
use miden_assembly::Library;
use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
use miden_processor::DeserializationError;
use semver::Version;
use super::AccountType;
use crate::errors::AccountComponentTemplateError;
mod storage;
pub use storage::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AccountComponentTemplate {
metadata: AccountComponentMetadata,
library: Library,
}
impl AccountComponentTemplate {
pub fn new(metadata: AccountComponentMetadata, library: Library) -> Self {
Self { metadata, library }
}
pub fn metadata(&self) -> &AccountComponentMetadata {
&self.metadata
}
pub fn library(&self) -> &Library {
&self.library
}
}
impl Serializable for AccountComponentTemplate {
fn write_into<W: miden_core::utils::ByteWriter>(&self, target: &mut W) {
target.write(&self.metadata);
target.write(&self.library);
}
}
impl Deserializable for AccountComponentTemplate {
fn read_from<R: miden_core::utils::ByteReader>(
source: &mut R,
) -> Result<Self, miden_processor::DeserializationError> {
let metadata: AccountComponentMetadata = source.read()?;
let library = Library::read_from(source)?;
Ok(AccountComponentTemplate::new(metadata, library))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "std", serde(rename_all = "kebab-case"))]
pub struct AccountComponentMetadata {
name: String,
description: String,
version: Version,
supported_types: BTreeSet<AccountType>,
storage: Vec<StorageEntry>,
}
impl AccountComponentMetadata {
pub fn new(
name: String,
description: String,
version: Version,
targets: BTreeSet<AccountType>,
storage: Vec<StorageEntry>,
) -> Result<Self, AccountComponentTemplateError> {
let component = Self {
name,
description,
version,
supported_types: targets,
storage,
};
component.validate()?;
Ok(component)
}
pub fn get_placeholder_requirements(
&self,
) -> BTreeMap<StorageValueName, PlaceholderTypeRequirement> {
let mut templates = BTreeMap::new();
for entry in self.storage_entries() {
for (name, requirement) in entry.template_requirements() {
templates.insert(name, requirement);
}
}
templates
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn version(&self) -> &Version {
&self.version
}
pub fn supported_types(&self) -> &BTreeSet<AccountType> {
&self.supported_types
}
pub fn storage_entries(&self) -> &Vec<StorageEntry> {
&self.storage
}
fn validate(&self) -> Result<(), AccountComponentTemplateError> {
let mut all_slots: Vec<u8> =
self.storage.iter().flat_map(|entry| entry.slot_indices()).collect();
all_slots.sort_unstable();
if let Some(&first_slot) = all_slots.first()
&& first_slot != 0
{
return Err(AccountComponentTemplateError::StorageSlotsDoNotStartAtZero(first_slot));
}
for slots in all_slots.windows(2) {
if slots[1] == slots[0] {
return Err(AccountComponentTemplateError::DuplicateSlot(slots[0]));
}
if slots[1] != slots[0] + 1 {
return Err(AccountComponentTemplateError::NonContiguousSlots(slots[0], slots[1]));
}
}
let mut seen_names = BTreeSet::new();
for entry in self.storage_entries() {
entry.validate()?;
if let Some(name) = entry.name() {
let name_existed = !seen_names.insert(name.as_str());
if name_existed {
return Err(AccountComponentTemplateError::DuplicateEntryNames(name.clone()));
}
}
}
let mut seen_placeholder_names = BTreeSet::new();
for entry in self.storage_entries() {
for (name, _) in entry.template_requirements() {
if !seen_placeholder_names.insert(name.clone()) {
return Err(AccountComponentTemplateError::DuplicatePlaceholderName(name));
}
}
}
Ok(())
}
}
impl Serializable for AccountComponentMetadata {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.name.write_into(target);
self.description.write_into(target);
self.version.to_string().write_into(target);
self.supported_types.write_into(target);
self.storage.write_into(target);
}
}
impl Deserializable for AccountComponentMetadata {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
Ok(Self {
name: String::read_from(source)?,
description: String::read_from(source)?,
version: semver::Version::from_str(&String::read_from(source)?).map_err(
|err: semver::Error| DeserializationError::InvalidValue(err.to_string()),
)?,
supported_types: BTreeSet::<AccountType>::read_from(source)?,
storage: Vec::<StorageEntry>::read_from(source)?,
})
}
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet};
use std::string::ToString;
use assert_matches::assert_matches;
use miden_assembly::Assembler;
use miden_core::utils::{Deserializable, Serializable};
use miden_core::{Felt, FieldElement};
use semver::Version;
use super::FeltRepresentation;
use crate::AccountError;
use crate::account::component::FieldIdentifier;
use crate::account::component::template::storage::StorageEntry;
use crate::account::component::template::{
AccountComponentMetadata,
AccountComponentTemplate,
InitStorageData,
};
use crate::account::{AccountComponent, StorageValueName};
use crate::errors::AccountComponentTemplateError;
use crate::testing::account_code::CODE;
fn default_felt_array() -> [FeltRepresentation; 4] {
[
FeltRepresentation::from(Felt::ZERO),
FeltRepresentation::from(Felt::ZERO),
FeltRepresentation::from(Felt::ZERO),
FeltRepresentation::from(Felt::ZERO),
]
}
#[test]
fn contiguous_value_slots() {
let storage = vec![
StorageEntry::new_value(0, default_felt_array()),
StorageEntry::new_multislot(
FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()),
1..3,
vec![default_felt_array(), default_felt_array()],
),
];
let original_config = AccountComponentMetadata {
name: "test".into(),
description: "desc".into(),
version: Version::parse("0.1.0").unwrap(),
supported_types: BTreeSet::new(),
storage,
};
let serialized = original_config.to_toml().unwrap();
let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap();
assert_eq!(deserialized, original_config);
}
#[test]
fn new_non_contiguous_value_slots() {
let storage = vec![
StorageEntry::new_value(0, default_felt_array()),
StorageEntry::new_value(2, default_felt_array()),
];
let result = AccountComponentMetadata::new(
"test".into(),
"desc".into(),
Version::parse("0.1.0").unwrap(),
BTreeSet::new(),
storage,
);
assert_matches!(result, Err(AccountComponentTemplateError::NonContiguousSlots(0, 2)));
}
#[test]
fn binary_serde_roundtrip() {
let storage = vec![
StorageEntry::new_multislot(
FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()),
1..3,
vec![default_felt_array(), default_felt_array()],
),
StorageEntry::new_value(0, default_felt_array()),
];
let component_metadata = AccountComponentMetadata {
name: "test".into(),
description: "desc".into(),
version: Version::parse("0.1.0").unwrap(),
supported_types: BTreeSet::new(),
storage,
};
let library = Assembler::default().assemble_library([CODE]).unwrap();
let template = AccountComponentTemplate::new(component_metadata, library);
let _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap();
let serialized = template.to_bytes();
let deserialized = AccountComponentTemplate::read_from_bytes(&serialized).unwrap();
assert_eq!(deserialized, template);
}
#[test]
pub fn fail_on_duplicate_key() {
let toml_text = r#"
name = "Test Component"
description = "This is a test component"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
name = "map"
description = "A storage map entry"
slot = 0
values = [
{ key = "0x1", value = ["0x3", "0x1", "0x2", "0x3"] },
{ key = "0x1", value = ["0x1", "0x2", "0x3", "0x10"] }
]
"#;
let result = AccountComponentMetadata::from_toml(toml_text);
assert_matches!(result, Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)));
}
#[test]
pub fn fail_on_duplicate_placeholder_name() {
let toml_text = r#"
name = "Test Component"
description = "tests for two duplicate placeholders"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
name = "map"
slot = 0
values = [
{ key = "0x1", value = [{type = "felt", name = "test"}, "0x1", "0x2", "0x3"] },
{ key = "0x2", value = ["0x1", "0x2", "0x3", {type = "token_symbol", name = "test"}] }
]
"#;
let result = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
assert_matches::assert_matches!(
result,
AccountComponentTemplateError::DuplicatePlaceholderName(_)
);
}
#[test]
pub fn fail_duplicate_key_instance() {
let _ = color_eyre::install();
let toml_text = r#"
name = "Test Component"
description = "This is a test component"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
name = "map"
description = "A storage map entry"
slot = 0
values = [
{ key = ["0", "0", "0", "1"], value = ["0x9", "0x12", "0x31", "0x18"] },
{ key = { name="duplicate_key" }, value = ["0x1", "0x2", "0x3", "0x4"] }
]
"#;
let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
let library = Assembler::default().assemble_library([CODE]).unwrap();
let template = AccountComponentTemplate::new(metadata, library);
let init_storage_data = InitStorageData::new(
[(
StorageValueName::new("map.duplicate_key").unwrap(),
"0x0000000000000000000000000000000000000000000000000100000000000000".to_string(),
)],
BTreeMap::new(),
);
let account_component = AccountComponent::from_template(&template, &init_storage_data);
assert_matches!(
account_component,
Err(AccountError::AccountComponentTemplateInstantiationError(
AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)
))
);
let valid_init_storage_data = InitStorageData::new(
[(StorageValueName::new("map.duplicate_key").unwrap(), "0x30".to_string())],
BTreeMap::new(),
);
AccountComponent::from_template(&template, &valid_init_storage_data).unwrap();
}
}