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

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