miden_objects/account/component/template/storage/
toml.rs

1use alloc::{
2    collections::BTreeMap,
3    string::{String, ToString},
4    vec::Vec,
5};
6use core::fmt;
7
8use serde::{
9    Deserialize, Deserializer, Serialize, Serializer,
10    de::{self, Error, MapAccess, SeqAccess, Visitor, value::MapAccessDeserializer},
11    ser::{SerializeMap, SerializeStruct},
12};
13use thiserror::Error;
14use vm_core::Felt;
15
16use super::{
17    FeltRepresentation, InitStorageData, MapEntry, MapRepresentation, MultiWordRepresentation,
18    StorageEntry, StorageValueNameError, WordRepresentation, placeholder::TemplateType,
19};
20use crate::{
21    account::{
22        AccountComponentMetadata, StorageValueName,
23        component::{
24            FieldIdentifier,
25            template::storage::placeholder::{TEMPLATE_REGISTRY, TemplateFelt},
26        },
27    },
28    errors::AccountComponentTemplateError,
29    utils::parse_hex_string_as_word,
30};
31
32// ACCOUNT COMPONENT METADATA TOML FROM/TO
33// ================================================================================================
34
35impl AccountComponentMetadata {
36    /// Deserializes `toml_string` and validates the resulting [AccountComponentMetadata]
37    ///
38    /// # Errors
39    ///
40    /// - If deserialization fails
41    /// - If the template specifies storage slots with duplicates.
42    /// - If the template includes slot numbers that do not start at zero.
43    /// - If storage slots in the template are not contiguous.
44    pub fn from_toml(toml_string: &str) -> Result<Self, AccountComponentTemplateError> {
45        let component: AccountComponentMetadata = toml::from_str(toml_string)
46            .map_err(AccountComponentTemplateError::TomlDeserializationError)?;
47
48        component.validate()?;
49        Ok(component)
50    }
51
52    /// Serializes the account component template into a TOML string.
53    pub fn as_toml(&self) -> Result<String, AccountComponentTemplateError> {
54        let toml =
55            toml::to_string(self).map_err(AccountComponentTemplateError::TomlSerializationError)?;
56        Ok(toml)
57    }
58}
59
60// WORD REPRESENTATION SERIALIZATION
61// ================================================================================================
62
63impl Serialize for WordRepresentation {
64    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65    where
66        S: Serializer,
67    {
68        match self {
69            WordRepresentation::Template { identifier, r#type } => {
70                let mut state = serializer.serialize_struct("WordRepresentation", 3)?;
71                state.serialize_field("name", &identifier.name())?;
72                state.serialize_field("description", &identifier.description())?;
73                state.serialize_field("type", r#type)?;
74                state.end()
75            },
76            WordRepresentation::Value { identifier, value } => {
77                let mut state = serializer.serialize_struct("WordRepresentation", 3)?;
78
79                state.serialize_field("name", &identifier.as_ref().map(|id| id.name()))?;
80                state.serialize_field(
81                    "description",
82                    &identifier.as_ref().map(|id| id.description()),
83                )?;
84                state.serialize_field("value", value)?;
85                state.end()
86            },
87        }
88    }
89}
90
91impl<'de> Deserialize<'de> for WordRepresentation {
92    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93    where
94        D: Deserializer<'de>,
95    {
96        struct WordRepresentationVisitor;
97
98        impl<'de> Visitor<'de> for WordRepresentationVisitor {
99            type Value = WordRepresentation;
100
101            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
102                formatter.write_str("a string or a map representing a WordRepresentation")
103            }
104
105            // A bare string is interpreted it as a Value variant.
106            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
107            where
108                E: Error,
109            {
110                let parsed_value = parse_hex_string_as_word(value).map_err(|_err| {
111                    E::invalid_value(
112                        serde::de::Unexpected::Str(value),
113                        &"a valid hexadecimal string",
114                    )
115                })?;
116                Ok(parsed_value.into())
117            }
118
119            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
120            where
121                E: Error,
122            {
123                self.visit_str(&value)
124            }
125
126            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
127            where
128                A: SeqAccess<'de>,
129            {
130                // Deserialize as a list of felt representations
131                let elements: Vec<FeltRepresentation> =
132                    Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?;
133                if elements.len() != 4 {
134                    return Err(Error::invalid_length(
135                        elements.len(),
136                        &"expected an array of 4 elements",
137                    ));
138                }
139                let value: [FeltRepresentation; 4] =
140                    elements.try_into().expect("length was checked");
141                Ok(WordRepresentation::new_value(value, None))
142            }
143
144            fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
145            where
146                M: MapAccess<'de>,
147            {
148                #[derive(Deserialize, Debug)]
149                struct WordRepresentationHelper {
150                    name: Option<String>,
151                    description: Option<String>,
152                    // The "value" field (if present) must be an array of 4 FeltRepresentations.
153                    value: Option<[FeltRepresentation; 4]>,
154                    #[serde(rename = "type")]
155                    r#type: Option<TemplateType>,
156                }
157
158                let helper =
159                    WordRepresentationHelper::deserialize(MapAccessDeserializer::new(map))?;
160
161                if let Some(value) = helper.value {
162                    let identifier = helper
163                        .name
164                        .map(|n| parse_field_identifier::<M::Error>(n, helper.description.clone()))
165                        .transpose()?;
166                    Ok(WordRepresentation::Value { value, identifier })
167                } else {
168                    // Otherwise, we expect a Template variant (name is required for identification)
169                    let identifier = expect_parse_field_identifier::<M::Error>(
170                        helper.name,
171                        helper.description,
172                        "word template",
173                    )?;
174                    let r#type = helper.r#type.unwrap_or_else(TemplateType::native_word);
175                    Ok(WordRepresentation::Template { r#type, identifier })
176                }
177            }
178        }
179
180        deserializer.deserialize_any(WordRepresentationVisitor)
181    }
182}
183
184// FELT REPRESENTATION SERIALIZATION
185// ================================================================================================
186
187impl Serialize for FeltRepresentation {
188    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
189    where
190        S: Serializer,
191    {
192        match self {
193            FeltRepresentation::Value { identifier, value } => {
194                let hex = value.to_string();
195                if identifier.is_none() {
196                    serializer.serialize_str(&hex)
197                } else {
198                    let mut state = serializer.serialize_struct("FeltRepresentation", 3)?;
199                    if let Some(id) = identifier {
200                        state.serialize_field("name", &id.name)?;
201                        state.serialize_field("description", &id.description)?;
202                    }
203                    state.serialize_field("value", &hex)?;
204                    state.end()
205                }
206            },
207            FeltRepresentation::Template { identifier, r#type } => {
208                let mut state = serializer.serialize_struct("FeltRepresentation", 3)?;
209                state.serialize_field("name", &identifier.name)?;
210                state.serialize_field("description", &identifier.description)?;
211                state.serialize_field("type", r#type)?;
212                state.end()
213            },
214        }
215    }
216}
217
218impl<'de> Deserialize<'de> for FeltRepresentation {
219    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
220    where
221        D: Deserializer<'de>,
222    {
223        // Felts can be deserialized as either:
224        //
225        // - Scalars (parsed from strings)
226        // - A table object that can or cannot hardcode a value. If not present, this is a
227        //   placeholder type
228        #[derive(Deserialize)]
229        #[serde(untagged)]
230        enum Intermediate {
231            Map {
232                name: Option<String>,
233                description: Option<String>,
234                #[serde(default)]
235                value: Option<String>,
236                #[serde(rename = "type")]
237                r#type: Option<TemplateType>,
238            },
239            Scalar(String),
240        }
241
242        let intermediate = Intermediate::deserialize(deserializer)?;
243        match intermediate {
244            Intermediate::Scalar(s) => {
245                let felt = Felt::parse_felt(&s)
246                    .map_err(|e| D::Error::custom(format!("failed to parse Felt: {}", e)))?;
247                Ok(FeltRepresentation::Value { identifier: None, value: felt })
248            },
249            Intermediate::Map { name, description, value, r#type } => {
250                // Get the defined type, or the default if it was not specified
251                let felt_type = r#type.unwrap_or_else(TemplateType::native_felt);
252                if let Some(val_str) = value {
253                    // Parse into felt from the input string
254                    let felt =
255                        TEMPLATE_REGISTRY.try_parse_felt(&felt_type, &val_str).map_err(|e| {
256                            D::Error::custom(format!("failed to parse {felt_type} as Felt: {}", e))
257                        })?;
258                    let identifier = name
259                        .map(|n| parse_field_identifier::<D::Error>(n, description.clone()))
260                        .transpose()?;
261                    Ok(FeltRepresentation::Value { identifier, value: felt })
262                } else {
263                    // No value provided, so this is a placeholder
264                    let identifier = expect_parse_field_identifier::<D::Error>(
265                        name,
266                        description,
267                        "map template",
268                    )?;
269                    Ok(FeltRepresentation::Template { r#type: felt_type, identifier })
270                }
271            },
272        }
273    }
274}
275
276// STORAGE VALUES
277// ================================================================================================
278
279/// Represents the type of values that can be found in a storage slot's `values` field.
280#[derive(Debug, Deserialize, Serialize)]
281#[serde(untagged)]
282enum StorageValues {
283    /// List of individual words (for multi-slot entries).
284    Words(Vec<[FeltRepresentation; 4]>),
285    /// List of key-value entries (for map storage slots).
286    MapEntries(Vec<MapEntry>),
287}
288
289// STORAGE ENTRY SERIALIZATION
290// ================================================================================================
291
292#[derive(Default, Debug, Deserialize, Serialize)]
293struct RawStorageEntry {
294    #[serde(flatten)]
295    identifier: Option<FieldIdentifier>,
296    slot: Option<u8>,
297    slots: Option<Vec<u8>>,
298    #[serde(rename = "type")]
299    word_type: Option<TemplateType>,
300    value: Option<[FeltRepresentation; 4]>,
301    values: Option<StorageValues>,
302}
303
304impl From<StorageEntry> for RawStorageEntry {
305    fn from(entry: StorageEntry) -> Self {
306        match entry {
307            StorageEntry::Value { slot, word_entry } => match word_entry {
308                WordRepresentation::Value { identifier, value } => RawStorageEntry {
309                    slot: Some(slot),
310                    identifier,
311                    value: Some(value),
312                    ..Default::default()
313                },
314                WordRepresentation::Template { identifier, r#type } => RawStorageEntry {
315                    slot: Some(slot),
316                    identifier: Some(identifier),
317                    word_type: Some(r#type),
318                    ..Default::default()
319                },
320            },
321            StorageEntry::Map { slot, map } => RawStorageEntry {
322                slot: Some(slot),
323                identifier: Some(FieldIdentifier {
324                    name: map.name().clone(),
325                    description: map.description().cloned(),
326                }),
327                values: Some(StorageValues::MapEntries(map.into())),
328                ..Default::default()
329            },
330            StorageEntry::MultiSlot { slots, word_entries } => match word_entries {
331                MultiWordRepresentation::Value { identifier, values } => RawStorageEntry {
332                    slot: None,
333                    identifier: Some(identifier),
334                    slots: Some(slots.collect()),
335                    values: Some(StorageValues::Words(values)),
336                    ..Default::default()
337                },
338            },
339        }
340    }
341}
342
343impl Serialize for StorageEntry {
344    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
345    where
346        S: Serializer,
347    {
348        let raw_storage_entry: RawStorageEntry = self.clone().into();
349        raw_storage_entry.serialize(serializer)
350    }
351}
352
353impl<'de> Deserialize<'de> for StorageEntry {
354    fn deserialize<D>(deserializer: D) -> Result<StorageEntry, D::Error>
355    where
356        D: Deserializer<'de>,
357    {
358        let raw = RawStorageEntry::deserialize(deserializer)?;
359
360        if let Some(word_entry) = raw.value {
361            // If a value was provided, this is a WordRepresentation::Value entry
362            let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "value entry"))?;
363            let identifier = raw.identifier;
364            Ok(StorageEntry::Value {
365                slot,
366                word_entry: WordRepresentation::Value { value: word_entry, identifier },
367            })
368        } else if let Some(StorageValues::MapEntries(map_entries)) = raw.values {
369            // If `values` field contains key/value pairs, deserialize as map
370            let identifier =
371                raw.identifier.ok_or_else(|| missing_field_for("identifier", "map entry"))?;
372            let name = identifier.name;
373            let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "map entry"))?;
374            let mut map = MapRepresentation::new(map_entries, name);
375            if let Some(desc) = identifier.description {
376                map = map.with_description(desc);
377            }
378            Ok(StorageEntry::Map { slot, map })
379        } else if let Some(StorageValues::Words(values)) = raw.values {
380            let identifier = raw
381                .identifier
382                .ok_or_else(|| missing_field_for("identifier", "multislot entry"))?;
383
384            let mut slots =
385                raw.slots.ok_or_else(|| missing_field_for("slots", "multislot entry"))?;
386
387            // Sort so we can check contiguity
388            slots.sort_unstable();
389            for pair in slots.windows(2) {
390                if pair[1] != pair[0] + 1 {
391                    return Err(serde::de::Error::custom(format!(
392                        "`slots` in the `{}` storage entry are not contiguous",
393                        identifier.name
394                    )));
395                }
396            }
397            let start = slots[0];
398            let end = slots.last().expect("checked validity") + 1;
399            Ok(StorageEntry::new_multislot(identifier, start..end, values))
400        } else if let Some(word_type) = raw.word_type {
401            // If a type was provided instead, this is a WordRepresentation::Template entry
402            let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "single-slot entry"))?;
403            let identifier = raw
404                .identifier
405                .ok_or_else(|| missing_field_for("identifier", "single-slot entry"))?;
406            let word_entry = WordRepresentation::Template { r#type: word_type, identifier };
407            Ok(StorageEntry::Value { slot, word_entry })
408        } else {
409            Err(D::Error::custom("placeholder storage entries require the `type` field"))
410        }
411    }
412}
413
414// INIT STORAGE DATA
415// ================================================================================================
416
417impl InitStorageData {
418    /// Creates an instance of [`InitStorageData`] from a TOML string.
419    ///
420    /// This method parses the provided TOML and flattens nested tables into
421    /// dot‑separated keys using [`StorageValueName`] as keys. All values are converted to plain
422    /// strings (so that, for example, `key = 10` and `key = "10"` both yield
423    /// `String::from("10")` as the value).
424    ///
425    /// # Errors
426    ///
427    /// - If duplicate keys or empty tables are found in the string
428    /// - If the TOML string includes arrays
429    pub fn from_toml(toml_str: &str) -> Result<Self, InitStorageDataError> {
430        let value: toml::Value = toml::from_str(toml_str)?;
431        let mut placeholders = BTreeMap::new();
432        // Start with an empty prefix (i.e. the default, which is an empty string)
433        Self::flatten_parse_toml_value(StorageValueName::empty(), &value, &mut placeholders)?;
434        Ok(InitStorageData::new(placeholders))
435    }
436
437    /// Recursively flattens a TOML `Value` into a flat mapping.
438    ///
439    /// When recursing into nested tables, keys are combined using
440    /// [`StorageValueName::with_suffix`]. If an encountered table is empty (and not the top-level),
441    /// an error is returned. Arrays are not supported.
442    fn flatten_parse_toml_value(
443        prefix: StorageValueName,
444        value: &toml::Value,
445        map: &mut BTreeMap<StorageValueName, String>,
446    ) -> Result<(), InitStorageDataError> {
447        match value {
448            toml::Value::Table(table) => {
449                // If this is not the root and the table is empty, error
450                if !prefix.as_str().is_empty() && table.is_empty() {
451                    return Err(InitStorageDataError::EmptyTable(prefix.as_str().into()));
452                }
453                for (key, val) in table {
454                    // Create a new key and combine it with the current prefix.
455                    let new_key = StorageValueName::new(key.to_string())
456                        .map_err(InitStorageDataError::InvalidStorageValueName)?;
457                    let new_prefix = prefix.clone().with_suffix(&new_key);
458                    Self::flatten_parse_toml_value(new_prefix, val, map)?;
459                }
460            },
461            toml::Value::Array(_) => {
462                return Err(InitStorageDataError::ArraysNotSupported);
463            },
464            toml_value => {
465                // Get the string value, or convert to string if it's some other type
466                let value = match toml_value {
467                    toml::Value::String(s) => s.clone(),
468                    _ => value.to_string(),
469                };
470                map.insert(prefix, value);
471            },
472        }
473        Ok(())
474    }
475}
476
477#[derive(Debug, Error)]
478pub enum InitStorageDataError {
479    #[error("failed to parse TOML")]
480    InvalidToml(#[from] toml::de::Error),
481
482    #[error("empty table encountered for key `{0}`")]
483    EmptyTable(String),
484
485    #[error("invalid input: arrays are not supported")]
486    ArraysNotSupported,
487
488    #[error("invalid storage value name")]
489    InvalidStorageValueName(#[source] StorageValueNameError),
490}
491
492impl Serialize for FieldIdentifier {
493    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
494    where
495        S: Serializer,
496    {
497        let mut map = serializer.serialize_map(Some(2))?;
498        map.serialize_entry("name", &self.name)?;
499        map.serialize_entry("description", &self.description)?;
500        map.end()
501    }
502}
503
504struct FieldIdentifierVisitor;
505
506impl<'de> Visitor<'de> for FieldIdentifierVisitor {
507    type Value = FieldIdentifier;
508
509    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
510        formatter.write_str("a map with 'name' and optionally 'description'")
511    }
512
513    fn visit_map<M>(self, mut map: M) -> Result<FieldIdentifier, M::Error>
514    where
515        M: MapAccess<'de>,
516    {
517        let mut name = None;
518        let mut description = None;
519        while let Some(key) = map.next_key::<String>()? {
520            match key.as_str() {
521                "name" => {
522                    name = Some(map.next_value()?);
523                },
524                "description" => {
525                    let d: String = map.next_value()?;
526                    // Normalize empty or whitespace-only strings into None
527                    description = if d.trim().is_empty() { None } else { Some(d) };
528                },
529                _ => {
530                    // Ignore other values as FieldIdentifiers are flattened within other structs
531                    let _: de::IgnoredAny = map.next_value()?;
532                },
533            }
534        }
535        let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
536        Ok(FieldIdentifier { name, description })
537    }
538}
539
540impl<'de> Deserialize<'de> for FieldIdentifier {
541    fn deserialize<D>(deserializer: D) -> Result<FieldIdentifier, D::Error>
542    where
543        D: Deserializer<'de>,
544    {
545        deserializer.deserialize_map(FieldIdentifierVisitor)
546    }
547}
548
549// UTILS / HELPERS
550// ================================================================================================
551
552fn missing_field_for<E: serde::de::Error>(field: &str, context: &str) -> E {
553    E::custom(format!("missing '{}' field for {}", field, context))
554}
555
556/// Checks than an optional (but expected) name field has been defined and is correct.
557fn expect_parse_field_identifier<E: serde::de::Error>(
558    n: Option<String>,
559    description: Option<String>,
560    context: &str,
561) -> Result<FieldIdentifier, E> {
562    let name = n.ok_or_else(|| missing_field_for("name", context))?;
563    parse_field_identifier(name, description)
564}
565
566/// Tries to parse a string into a [FieldIdentifier].
567fn parse_field_identifier<E: serde::de::Error>(
568    n: String,
569    description: Option<String>,
570) -> Result<FieldIdentifier, E> {
571    StorageValueName::new(n)
572        .map_err(|err| E::custom(format!("invalid `name`: {err}")))
573        .map(|storage_name| {
574            if let Some(desc) = description {
575                FieldIdentifier::with_description(storage_name, desc)
576            } else {
577                FieldIdentifier::with_name(storage_name)
578            }
579        })
580}
581
582// TESTS
583// ================================================================================================
584
585#[cfg(test)]
586mod tests {
587    use alloc::string::ToString;
588    use core::error::Error;
589
590    use super::*;
591    use crate::account::component::toml::InitStorageDataError;
592
593    #[test]
594    fn from_toml_str_with_nested_table_and_flattened() {
595        let toml_table = r#"
596            [token_metadata]
597            max_supply = "1000000000"
598            symbol = "ETH"
599            decimals = "9"
600        "#;
601
602        let toml_inline = r#"
603            token_metadata.max_supply = "1000000000"
604            token_metadata.symbol = "ETH"
605            token_metadata.decimals = "9"
606        "#;
607
608        let storage_table = InitStorageData::from_toml(toml_table).unwrap();
609        let storage_inline = InitStorageData::from_toml(toml_inline).unwrap();
610
611        assert_eq!(storage_table.placeholders(), storage_inline.placeholders());
612    }
613
614    #[test]
615    fn from_toml_str_with_deeply_nested_tables() {
616        let toml_str = r#"
617            [a]
618            b = "0xb"
619
620            [a.c]
621            d = "0xd"
622
623            [x.y.z]
624            w = 42 # NOTE: This gets parsed as string
625        "#;
626
627        let storage = InitStorageData::from_toml(toml_str).expect("Failed to parse TOML");
628        let key1 = StorageValueName::new("a.b".to_string()).unwrap();
629        let key2 = StorageValueName::new("a.c.d".to_string()).unwrap();
630        let key3 = StorageValueName::new("x.y.z.w".to_string()).unwrap();
631
632        assert_eq!(storage.get(&key1).unwrap(), "0xb");
633        assert_eq!(storage.get(&key2).unwrap(), "0xd");
634        assert_eq!(storage.get(&key3).unwrap(), "42");
635    }
636
637    #[test]
638    fn test_error_on_array() {
639        let toml_str = r#"
640            token_metadata.v = [1, 2, 3]
641        "#;
642
643        let result = InitStorageData::from_toml(toml_str);
644        assert_matches::assert_matches!(
645            result.unwrap_err(),
646            InitStorageDataError::ArraysNotSupported
647        );
648    }
649
650    #[test]
651    fn error_on_empty_subtable() {
652        let toml_str = r#"
653            [a]
654            b = {}
655        "#;
656
657        let result = InitStorageData::from_toml(toml_str);
658        assert_matches::assert_matches!(result.unwrap_err(), InitStorageDataError::EmptyTable(_));
659    }
660
661    #[test]
662    fn error_on_duplicate_keys() {
663        let toml_str = r#"
664            token_metadata.max_supply = "1000000000"
665            token_metadata.max_supply = "500000000"
666        "#;
667
668        let result = InitStorageData::from_toml(toml_str).unwrap_err();
669        // TOML does not support duplicate keys
670        assert_matches::assert_matches!(result, InitStorageDataError::InvalidToml(_));
671        assert!(result.source().unwrap().to_string().contains("duplicate"));
672    }
673}