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

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