use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::ops::Range;
use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
use miden_core::{Felt, FieldElement};
use miden_processor::DeserializationError;
mod entry_content;
pub use entry_content::*;
use super::AccountComponentTemplateError;
use crate::Word;
use crate::account::StorageSlot;
mod placeholder;
pub use placeholder::{
PlaceholderTypeRequirement,
StorageValueName,
StorageValueNameError,
TemplateType,
TemplateTypeError,
};
mod init_storage_data;
pub use init_storage_data::InitStorageData;
#[cfg(feature = "std")]
pub mod toml;
pub type TemplateRequirementsIter<'a> =
Box<dyn Iterator<Item = (StorageValueName, PlaceholderTypeRequirement)> + 'a>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldIdentifier {
pub name: StorageValueName,
pub description: Option<String>,
}
impl FieldIdentifier {
pub fn with_name(name: StorageValueName) -> Self {
Self { name, description: None }
}
pub fn with_description(name: StorageValueName, description: impl Into<String>) -> Self {
Self {
name,
description: Some(description.into()),
}
}
pub fn name(&self) -> &StorageValueName {
&self.name
}
pub fn description(&self) -> Option<&String> {
self.description.as_ref()
}
}
impl From<StorageValueName> for FieldIdentifier {
fn from(value: StorageValueName) -> Self {
FieldIdentifier::with_name(value)
}
}
impl Serializable for FieldIdentifier {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(&self.name);
target.write(&self.description);
}
}
impl Deserializable for FieldIdentifier {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let name = StorageValueName::read_from(source)?;
let description = Option::<String>::read_from(source)?;
Ok(FieldIdentifier { name, description })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub enum StorageEntry {
Value {
slot: u8,
word_entry: WordRepresentation,
},
Map {
slot: u8,
map: MapRepresentation,
},
MultiSlot {
slots: Range<u8>,
word_entries: MultiWordRepresentation,
},
}
impl StorageEntry {
pub fn new_value(slot: u8, word_entry: impl Into<WordRepresentation>) -> Self {
StorageEntry::Value { slot, word_entry: word_entry.into() }
}
pub fn new_map(slot: u8, map: MapRepresentation) -> Self {
StorageEntry::Map { slot, map }
}
pub fn new_multislot(
identifier: FieldIdentifier,
slots: Range<u8>,
values: Vec<[FeltRepresentation; 4]>,
) -> Self {
StorageEntry::MultiSlot {
slots,
word_entries: MultiWordRepresentation::Value { identifier, values },
}
}
pub fn name(&self) -> Option<&StorageValueName> {
match self {
StorageEntry::Value { word_entry, .. } => word_entry.name(),
StorageEntry::Map { map, .. } => Some(map.name()),
StorageEntry::MultiSlot { word_entries, .. } => match word_entries {
MultiWordRepresentation::Value { identifier, .. } => Some(&identifier.name),
},
}
}
pub fn slot_indices(&self) -> Range<u8> {
match self {
StorageEntry::MultiSlot { slots, .. } => slots.clone(),
StorageEntry::Value { slot, .. } | StorageEntry::Map { slot, .. } => *slot..*slot + 1,
}
}
pub fn template_requirements(&self) -> TemplateRequirementsIter<'_> {
match self {
StorageEntry::Value { word_entry, .. } => {
word_entry.template_requirements(StorageValueName::empty())
},
StorageEntry::Map { map, .. } => map.template_requirements(),
StorageEntry::MultiSlot { word_entries, .. } => match word_entries {
MultiWordRepresentation::Value { identifier, values } => {
Box::new(values.iter().flat_map(move |word| {
word.iter()
.flat_map(move |f| f.template_requirements(identifier.name.clone()))
}))
},
},
}
}
pub fn try_build_storage_slots(
&self,
init_storage_data: &InitStorageData,
) -> Result<Vec<StorageSlot>, AccountComponentTemplateError> {
match self {
StorageEntry::Value { word_entry, .. } => {
let slot =
word_entry.try_build_word(init_storage_data, StorageValueName::empty())?;
Ok(vec![StorageSlot::Value(slot)])
},
StorageEntry::Map { map, .. } => {
let storage_map = map.try_build_map(init_storage_data)?;
Ok(vec![StorageSlot::Map(storage_map)])
},
StorageEntry::MultiSlot { word_entries, .. } => {
match word_entries {
MultiWordRepresentation::Value { identifier, values } => {
Ok(values
.iter()
.map(|word_repr| {
let mut result = [Felt::ZERO; 4];
for (index, felt_repr) in word_repr.iter().enumerate() {
result[index] = felt_repr.try_build_felt(
init_storage_data,
identifier.name.clone(),
)?;
}
Ok(StorageSlot::Value(Word::from(result)))
})
.collect::<Result<Vec<StorageSlot>, _>>()?)
},
}
},
}
}
pub(super) fn validate(&self) -> Result<(), AccountComponentTemplateError> {
match self {
StorageEntry::Map { map, .. } => map.validate(),
StorageEntry::MultiSlot { slots, word_entries, .. } => {
if slots.len() == 1 {
return Err(AccountComponentTemplateError::MultiSlotSpansOneSlot);
}
if slots.len() != word_entries.num_words() {
return Err(AccountComponentTemplateError::MultiSlotArityMismatch);
}
word_entries.validate()
},
StorageEntry::Value { word_entry, .. } => Ok(word_entry.validate()?),
}
}
}
impl Serializable for StorageEntry {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
match self {
StorageEntry::Value { slot, word_entry } => {
target.write_u8(0u8);
target.write_u8(*slot);
target.write(word_entry);
},
StorageEntry::Map { slot, map } => {
target.write_u8(1u8);
target.write_u8(*slot);
target.write(map);
},
StorageEntry::MultiSlot { word_entries, slots } => {
target.write_u8(2u8);
target.write(word_entries);
target.write(slots.start);
target.write(slots.end);
},
}
}
}
impl Deserializable for StorageEntry {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let variant_tag = source.read_u8()?;
match variant_tag {
0 => {
let slot = source.read_u8()?;
let word_entry: WordRepresentation = source.read()?;
Ok(StorageEntry::Value { slot, word_entry })
},
1 => {
let slot = source.read_u8()?;
let map: MapRepresentation = source.read()?;
Ok(StorageEntry::Map { slot, map })
},
2 => {
let word_entries: MultiWordRepresentation = source.read()?;
let slots_start: u8 = source.read()?;
let slots_end: u8 = source.read()?;
Ok(StorageEntry::MultiSlot {
slots: slots_start..slots_end,
word_entries,
})
},
_ => Err(DeserializationError::InvalidValue(format!(
"unknown variant tag '{variant_tag}' for StorageEntry"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
pub struct MapEntry {
key: WordRepresentation,
value: WordRepresentation,
}
impl MapEntry {
pub fn new(key: impl Into<WordRepresentation>, value: impl Into<WordRepresentation>) -> Self {
Self { key: key.into(), value: value.into() }
}
pub fn key(&self) -> &WordRepresentation {
&self.key
}
pub fn value(&self) -> &WordRepresentation {
&self.value
}
pub fn into_parts(self) -> (WordRepresentation, WordRepresentation) {
let MapEntry { key, value } = self;
(key, value)
}
pub fn template_requirements(
&self,
placeholder_prefix: StorageValueName,
) -> TemplateRequirementsIter<'_> {
let key_iter = self.key.template_requirements(placeholder_prefix.clone());
let value_iter = self.value.template_requirements(placeholder_prefix);
Box::new(key_iter.chain(value_iter))
}
}
impl Serializable for MapEntry {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.key.write_into(target);
self.value.write_into(target);
}
}
impl Deserializable for MapEntry {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let key = WordRepresentation::read_from(source)?;
let value = WordRepresentation::read_from(source)?;
Ok(MapEntry { key, value })
}
}
#[cfg(test)]
mod tests {
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::ToString;
use core::error::Error;
use core::panic;
use miden_assembly::Assembler;
use miden_core::utils::{Deserializable, Serializable};
use miden_core::{EMPTY_WORD, Felt, Word};
use semver::Version;
use crate::account::component::FieldIdentifier;
use crate::account::component::template::storage::placeholder::TemplateType;
use crate::account::component::template::{
AccountComponentMetadata,
InitStorageData,
MapEntry,
MapRepresentation,
StorageValueName,
};
use crate::account::{
AccountComponent,
AccountComponentTemplate,
AccountType,
FeltRepresentation,
StorageEntry,
StorageSlot,
TemplateTypeError,
WordRepresentation,
};
use crate::errors::AccountComponentTemplateError;
use crate::testing::account_code::CODE;
use crate::{AccountError, word};
#[test]
fn test_storage_entry_serialization() {
let felt_array: [FeltRepresentation; 4] = [
FeltRepresentation::from(Felt::new(0xabc)),
FeltRepresentation::from(Felt::new(1218)),
FeltRepresentation::from(Felt::new(0xdba3)),
FeltRepresentation::new_template(
TemplateType::native_felt(),
StorageValueName::new("slot3").unwrap(),
)
.with_description("dummy description"),
];
let test_word: Word = word!("0x000001");
let test_word = test_word.map(FeltRepresentation::from);
let map_representation = MapRepresentation::new_value(
vec![
MapEntry {
key: WordRepresentation::new_template(
TemplateType::native_word(),
StorageValueName::new("foo").unwrap().into(),
),
value: WordRepresentation::new_value(test_word.clone(), None),
},
MapEntry {
key: WordRepresentation::new_value(test_word.clone(), None),
value: WordRepresentation::new_template(
TemplateType::native_word(),
StorageValueName::new("bar").unwrap().into(),
),
},
MapEntry {
key: WordRepresentation::new_template(
TemplateType::native_word(),
StorageValueName::new("baz").unwrap().into(),
),
value: WordRepresentation::new_value(test_word, None),
},
],
StorageValueName::new("map").unwrap(),
)
.with_description("a storage map description");
let storage = vec![
StorageEntry::new_value(0, felt_array.clone()),
StorageEntry::new_map(1, map_representation),
StorageEntry::new_multislot(
FieldIdentifier::with_description(
StorageValueName::new("multi").unwrap(),
"Multi slot entry",
),
2..4,
vec![
[
FeltRepresentation::new_template(
TemplateType::native_felt(),
StorageValueName::new("test").unwrap(),
),
FeltRepresentation::new_template(
TemplateType::native_felt(),
StorageValueName::new("test2").unwrap(),
),
FeltRepresentation::new_template(
TemplateType::native_felt(),
StorageValueName::new("test3").unwrap(),
),
FeltRepresentation::new_template(
TemplateType::native_felt(),
StorageValueName::new("test4").unwrap(),
),
],
felt_array,
],
),
StorageEntry::new_value(
4,
WordRepresentation::new_template(
TemplateType::native_word(),
StorageValueName::new("single").unwrap().into(),
),
),
];
let config = AccountComponentMetadata {
name: "Test Component".into(),
description: "This is a test component".into(),
version: Version::parse("1.0.0").unwrap(),
supported_types: BTreeSet::from([AccountType::FungibleFaucet]),
storage,
};
let toml = config.to_toml().unwrap();
let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap();
assert_eq!(deserialized, config);
}
#[test]
pub fn toml_serde_roundtrip() {
let toml_text = r#"
name = "Test Component"
description = "This is a test component"
version = "1.0.1"
supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
[[storage]]
name = "map_entry"
slot = 0
values = [
{ key = "0x1", value = ["0x1","0x2","0x3","0"]},
{ key = "0x3", value = "0x123" },
{ key = { name = "map_key_template", description = "this tests that the default type is correctly set"}, value = "0x3" },
]
[[storage]]
name = "token_metadata"
description = "Contains metadata about the token associated to the faucet account"
slot = 1
value = [
{ type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, # placeholder
{ type = "token_symbol", value = "TST" }, # hardcoded non-felt type
{ type = "u8", name = "decimals", description = "Number of decimal places" }, # placeholder
{ value = "0" },
]
[[storage]]
name = "default_recallable_height"
slot = 2
type = "word"
"#;
let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
let requirements = component_metadata.get_placeholder_requirements();
assert_eq!(requirements.len(), 4);
let supply = requirements
.get(&StorageValueName::new("token_metadata.max_supply").unwrap())
.unwrap();
assert_eq!(supply.r#type.as_str(), "felt");
let decimals = requirements
.get(&StorageValueName::new("token_metadata.decimals").unwrap())
.unwrap();
assert_eq!(decimals.r#type.as_str(), "u8");
let default_recallable_height = requirements
.get(&StorageValueName::new("default_recallable_height").unwrap())
.unwrap();
assert_eq!(default_recallable_height.r#type.as_str(), "word");
let map_key_template = requirements
.get(&StorageValueName::new("map_entry.map_key_template").unwrap())
.unwrap();
assert_eq!(map_key_template.r#type.as_str(), "word");
let library = Assembler::default().assemble_library([CODE]).unwrap();
let template = AccountComponentTemplate::new(component_metadata, library);
let template_bytes = template.to_bytes();
let template_deserialized =
AccountComponentTemplate::read_from_bytes(&template_bytes).unwrap();
assert_eq!(template, template_deserialized);
let storage_placeholders = InitStorageData::new(
[
(
StorageValueName::new("map_entry.map_key_template").unwrap(),
"0x123".to_string(),
),
(
StorageValueName::new("token_metadata.max_supply").unwrap(),
20_000u64.to_string(),
),
(StorageValueName::new("token_metadata.decimals").unwrap(), "2800".into()),
(StorageValueName::new("default_recallable_height").unwrap(), "0".into()),
],
BTreeMap::new(),
);
let component = AccountComponent::from_template(&template, &storage_placeholders);
assert_matches::assert_matches!(
component,
Err(AccountError::AccountComponentTemplateInstantiationError(
AccountComponentTemplateError::StorageValueParsingError(
TemplateTypeError::ParseError { .. }
)
))
);
let storage_placeholders = InitStorageData::new(
[
(
StorageValueName::new("map_entry.map_key_template").unwrap(),
"0x123".to_string(),
),
(
StorageValueName::new("token_metadata.max_supply").unwrap(),
20_000u64.to_string(),
),
(StorageValueName::new("token_metadata.decimals").unwrap(), "128".into()),
(StorageValueName::new("default_recallable_height").unwrap(), "0x0".into()),
],
BTreeMap::new(),
);
let component = AccountComponent::from_template(&template, &storage_placeholders).unwrap();
assert_eq!(
component.supported_types(),
&[AccountType::FungibleFaucet, AccountType::RegularAccountImmutableCode]
.into_iter()
.collect()
);
let storage_map = component.storage_slots.first().unwrap();
match storage_map {
StorageSlot::Map(storage_map) => assert_eq!(storage_map.entries().count(), 3),
_ => panic!("should be map"),
}
let value_entry = component.storage_slots().get(2).unwrap();
match value_entry {
StorageSlot::Value(v) => {
assert_eq!(v, &EMPTY_WORD)
},
_ => panic!("should be value"),
}
let failed_instantiation =
AccountComponent::from_template(&template, &InitStorageData::default());
assert_matches::assert_matches!(
failed_instantiation,
Err(AccountError::AccountComponentTemplateInstantiationError(
AccountComponentTemplateError::PlaceholderValueNotProvided(_)
))
);
}
#[test]
fn test_no_duplicate_slot_names() {
let toml_text = r#"
name = "Test Component"
description = "This is a test component"
version = "1.0.1"
supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
[[storage]]
name = "test_duplicate"
slot = 0
type = "felt" # Felt is not a valid type for word slots
"#;
let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
assert_matches::assert_matches!(err, AccountComponentTemplateError::InvalidType(_, _))
}
#[test]
fn parses_and_instantiates_ecdsa_template() {
let toml = r#"
name = "ecdsa_auth"
description = "Ecdsa authentication component, for verifying ECDSA K256 Keccak signatures."
version = "0.1.0"
supported-types = ["RegularAccountUpdatableCode", "RegularAccountImmutableCode", "FungibleFaucet", "NonFungibleFaucet"]
[[storage]]
name = "ecdsa_pubkey"
description = "ecdsa public key"
slot = 0
type = "auth::ecdsa_k256_keccak::pub_key"
"#;
let metadata = AccountComponentMetadata::from_toml(toml).unwrap();
assert_eq!(metadata.storage_entries().len(), 1);
assert_eq!(metadata.storage_entries()[0].name().unwrap().as_str(), "ecdsa_pubkey");
let library = Assembler::default().assemble_library([CODE]).unwrap();
let template = AccountComponentTemplate::new(metadata, library);
let init_storage = InitStorageData::new(
[(StorageValueName::new("ecdsa_pubkey").unwrap(), "0x1234".into())],
BTreeMap::new(),
);
let component = AccountComponent::from_template(&template, &init_storage).unwrap();
let slot = component.storage_slots().first().expect("missing storage slot");
match slot {
StorageSlot::Value(word) => {
let expected = Word::parse(
"0x0000000000000000000000000000000000000000000000000000000000001234",
)
.unwrap();
assert_eq!(word, &expected);
},
_ => panic!("expected value storage slot"),
}
}
#[test]
fn map_template_can_build_from_entries() {
let map_name = StorageValueName::new("procedure_thresholds").unwrap();
let map_entry = StorageEntry::new_map(0, MapRepresentation::new_template(map_name.clone()));
let init_data = InitStorageData::from_toml(
r#"
procedure_thresholds = [
{ key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000010" },
{ key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = "0x0000000000000000000000000000000000000000000000000000000000000020" }
]
"#,
)
.unwrap();
let entries = init_data.map_entries(&map_name).expect("map entries missing");
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0],
(
Word::parse("0x0000000000000000000000000000000000000000000000000000000000000001",)
.unwrap(),
Word::parse("0x0000000000000000000000000000000000000000000000000000000000000010",)
.unwrap(),
)
);
let slots = map_entry.try_build_storage_slots(&init_data).unwrap();
assert_eq!(slots.len(), 1);
match &slots[0] {
StorageSlot::Map(storage_map) => {
assert_eq!(storage_map.num_entries(), 2);
let main_key = Word::parse(
"0x0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let main_value_expected = Word::parse(
"0x0000000000000000000000000000000000000000000000000000000000000010",
)
.unwrap();
assert_eq!(storage_map.get(&main_key), main_value_expected);
},
_ => panic!("expected map storage slot"),
}
}
#[test]
fn map_template_requires_entries() {
let map_name = StorageValueName::new("procedure_thresholds").unwrap();
let map_entry = StorageEntry::new_map(0, MapRepresentation::new_template(map_name.clone()));
let result = map_entry.try_build_storage_slots(&InitStorageData::default());
assert_matches::assert_matches!(
result,
Err(AccountComponentTemplateError::PlaceholderValueNotProvided(name))
if name.as_str() == "procedure_thresholds"
);
let init_data = InitStorageData::from_toml(
r#"
procedure_thresholds = []
"#,
)
.unwrap();
let result = map_entry.try_build_storage_slots(&init_data).unwrap();
assert_eq!(result.len(), 1);
match &result[0] {
StorageSlot::Map(storage_map) => assert_eq!(storage_map.num_entries(), 0),
_ => panic!("expected map storage slot"),
}
}
#[test]
fn map_placeholder_requirement_is_reported() {
let targets = [AccountType::RegularAccountImmutableCode].into_iter().collect();
let map =
MapRepresentation::new_template(StorageValueName::new("procedure_thresholds").unwrap())
.with_description("Configures procedure thresholds");
let metadata = AccountComponentMetadata::new(
"test".into(),
"desc".into(),
Version::new(1, 0, 0),
targets,
vec![StorageEntry::new_map(0, map)],
)
.unwrap();
let requirements = metadata.get_placeholder_requirements();
let requirement = requirements
.get(&StorageValueName::new("procedure_thresholds").unwrap())
.expect("map placeholder should be reported");
assert_eq!(requirement.r#type.as_str(), "map");
assert_eq!(requirement.description.as_deref(), Some("Configures procedure thresholds"),);
}
#[test]
fn toml_template_map_roundtrip() {
let toml_text = r#"
name = "Test Component"
description = "Component with templated map"
version = "1.0.0"
supported-types = ["RegularAccountImmutableCode"]
[[storage]]
name = "my_map"
description = "Some description"
slot = 0
type = "map"
"#;
let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
assert_eq!(metadata.storage_entries().len(), 1);
match metadata.storage_entries().first().unwrap() {
StorageEntry::Map { map, .. } => match map {
MapRepresentation::Template { identifier } => {
assert_eq!(identifier.name.as_str(), "my_map");
assert_eq!(identifier.description.as_deref(), Some("Some description"));
},
MapRepresentation::Value { .. } => panic!("expected template map"),
},
_ => panic!("expected map storage entry"),
}
let toml_roundtrip = metadata.to_toml().unwrap();
assert!(toml_roundtrip.contains("type = \"map\""));
}
#[test]
fn toml_map_with_empty_values_creates_value_map() {
let toml_text = r#"
name = "Test Component"
description = "Component with map having empty values"
version = "1.0.0"
supported-types = ["RegularAccountImmutableCode"]
[[storage]]
name = "executed_transactions"
description = "Map which stores executed transactions"
slot = 0
type = "map"
values = []
"#;
let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
assert_eq!(metadata.storage_entries().len(), 1);
match metadata.storage_entries().first().unwrap() {
StorageEntry::Map { map, .. } => match map {
MapRepresentation::Value { identifier, entries } => {
assert_eq!(identifier.name.as_str(), "executed_transactions");
assert_eq!(
identifier.description.as_deref(),
Some("Map which stores executed transactions")
);
assert!(entries.is_empty(), "Expected empty entries for map with values = []");
},
MapRepresentation::Template { .. } => {
panic!("expected value map with empty entries, not template map")
},
},
_ => panic!("expected map storage entry"),
}
}
#[test]
fn map_placeholder_populated_via_toml_array() {
let storage_entry = StorageEntry::new_map(
0,
MapRepresentation::new_template(StorageValueName::new("my_map").unwrap()),
);
let init_data = InitStorageData::from_toml(
r#"
my_map = [
{ key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000090" },
{ key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = ["1", "2", "3", "4"] }
]
other_placeholder = "0xAB"
"#,
)
.unwrap();
assert_eq!(
init_data.get(&StorageValueName::new("other_placeholder").unwrap()).unwrap(),
"0xAB"
);
let slots = storage_entry.try_build_storage_slots(&init_data).unwrap();
assert_eq!(slots.len(), 1);
match &slots[0] {
StorageSlot::Map(storage_map) => {
assert_eq!(storage_map.num_entries(), 2);
let second_value = Word::from([
Felt::new(1u64),
Felt::new(2u64),
Felt::new(3u64),
Felt::new(4u64),
]);
let second_key = Word::try_from(
"0x0000000000000000000000000000000000000000000000000000000000000002",
)
.unwrap();
assert_eq!(storage_map.get(&second_key), second_value);
},
_ => panic!("expected map storage slot"),
}
}
#[test]
fn toml_map_type_with_values_is_invalid() {
let toml_text = r#"
name = "Invalid"
description = "Invalid map"
version = "1.0.0"
supported-types = ["RegularAccountImmutableCode"]
[[storage]]
name = "bad_map"
slot = 0
type = "map"
values = [ { key = "0x1", value = "0x2" } ]
"#;
let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
match metadata.storage_entries().first().unwrap() {
StorageEntry::Map { map, .. } => match map {
MapRepresentation::Value { entries, .. } => {
assert_eq!(entries.len(), 1);
},
_ => panic!("expected static map"),
},
_ => panic!("expected map storage entry"),
}
}
#[test]
fn toml_map_values_with_non_map_type_is_invalid() {
let toml_text = r#"
name = "Invalid"
description = "Invalid map"
version = "1.0.0"
supported-types = ["RegularAccountImmutableCode"]
[[storage]]
name = "bad_map"
slot = 0
type = "word"
values = [ { key = "0x1", value = "0x2" } ]
"#;
let result = AccountComponentMetadata::from_toml(toml_text);
assert_matches::assert_matches!(
result,
Err(AccountComponentTemplateError::TomlDeserializationError(_))
);
}
#[test]
fn toml_fail_multislot_arity_mismatch() {
let toml_text = r#"
name = "Test Component"
description = "Test multislot arity mismatch"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
name = "multislot_test"
slots = [0, 1]
values = [
[ "0x1", "0x2", "0x3", "0x4" ]
]
"#;
let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
assert_matches::assert_matches!(err, AccountComponentTemplateError::MultiSlotArityMismatch);
}
#[test]
fn toml_fail_multislot_duplicate_slot() {
let toml_text = r#"
name = "Test Component"
description = "Test multislot duplicate slot"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
name = "multislot_duplicate"
slots = [0, 1]
values = [
[ "0x1", "0x2", "0x3", "0x4" ],
[ "0x5", "0x6", "0x7", "0x8" ]
]
[[storage]]
name = "multislot_duplicate"
slots = [1, 2]
values = [
[ "0x1", "0x2", "0x3", "0x4" ],
[ "0x5", "0x6", "0x7", "0x8" ]
]
"#;
let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
assert_matches::assert_matches!(err, AccountComponentTemplateError::DuplicateSlot(1));
}
#[test]
fn toml_fail_multislot_non_contiguous_slots() {
let toml_text = r#"
name = "Test Component"
description = "Test multislot non contiguous"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
name = "multislot_non_contiguous"
slots = [0, 2]
values = [
[ "0x1", "0x2", "0x3", "0x4" ],
[ "0x5", "0x6", "0x7", "0x8" ]
]
"#;
let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
assert!(err.source().unwrap().to_string().contains("are not contiguous"));
}
#[test]
fn toml_fail_duplicate_storage_entry_names() {
let toml_text = r#"
name = "Test Component"
description = "Component with duplicate storage entry names"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
# placeholder
name = "duplicate"
slot = 0
type = "word"
[[storage]]
name = "duplicate"
slot = 1
value = [ "0x1", "0x1", "0x1", "0x1" ]
"#;
let result = AccountComponentMetadata::from_toml(toml_text);
assert_matches::assert_matches!(
result.unwrap_err(),
AccountComponentTemplateError::DuplicateEntryNames(_)
);
}
#[test]
fn toml_fail_multislot_spans_one_slot() {
let toml_text = r#"
name = "Test Component"
description = "Test multislot spans one slot"
version = "1.0.1"
supported-types = ["RegularAccountImmutableCode"]
[[storage]]
name = "multislot_one_slot"
slots = [0]
values = [
[ "0x1", "0x2", "0x3", "0x4" ],
]
"#;
let result = AccountComponentMetadata::from_toml(toml_text);
assert_matches::assert_matches!(
result.unwrap_err(),
AccountComponentTemplateError::MultiSlotSpansOneSlot
);
}
#[test]
fn test_toml_multislot_success() {
let toml_text = r#"
name = "Test Component"
description = "A multi-slot success scenario"
version = "1.0.1"
supported-types = ["FungibleFaucet"]
[[storage]]
name = "multi_slot_example"
slots = [0, 1, 2]
values = [
["0x1", "0x2", "0x3", "0x4"],
["0x5", "0x6", "0x7", "0x8"],
["0x9", "0xa", "0xb", "0xc"]
]
"#;
let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
match &metadata.storage_entries()[0] {
StorageEntry::MultiSlot { slots, word_entries } => match word_entries {
crate::account::component::template::MultiWordRepresentation::Value {
identifier,
values,
} => {
assert_eq!(identifier.name.as_str(), "multi_slot_example");
assert_eq!(slots, &(0..3));
assert_eq!(values.len(), 3);
},
},
_ => panic!("expected multislot"),
}
}
}