Skip to main content

miden_note_schema/
codec.rs

1//! String codecs for named WIT leaf types.
2
3use std::{collections::BTreeMap, sync::Arc};
4
5use miden_field::{Felt, Word};
6use miden_field_repr::{FeltReader, FeltWriter, FromFeltRepr, ToFeltRepr};
7use miden_protocol::{account::AccountId, address::NetworkId, asset::AssetAmount};
8
9use crate::{Error, Result};
10
11/// The canonical WIT FQN for `felt`.
12pub const FELT_FQN: &str = "miden:base/core-types@1.0.0.felt";
13/// The canonical WIT FQN for `word`.
14pub const WORD_FQN: &str = "miden:base/core-types@1.0.0.word";
15/// The canonical WIT FQN for `account-id`.
16pub const ACCOUNT_ID_FQN: &str = "miden:base/core-types@1.0.0.account-id";
17/// The canonical WIT FQN for `asset-amount`.
18pub const ASSET_AMOUNT_FQN: &str = "miden:base/core-types@1.0.0.asset-amount";
19
20/// A protocol type whose schema leaf maps directly to an existing host type and standard codec.
21///
22/// This is the canonical standard-leaf definition used by schema traversal, Rust code generation,
23/// and author-codec registration. Named types outside this set remain schema-owned, including
24/// other records in the `miden:base/core-types` interface.
25#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub enum StandardLeaf {
27    /// The one-element Miden base-field type.
28    Felt,
29    /// A group of four Miden base-field elements.
30    Word,
31    /// A two-element protocol account identifier.
32    AccountId,
33    /// A validated fungible-asset amount.
34    AssetAmount,
35}
36
37impl StandardLeaf {
38    /// Every standard leaf in canonical registry order.
39    pub const ALL: [Self; 4] = [Self::Felt, Self::Word, Self::AccountId, Self::AssetAmount];
40
41    /// Returns the canonical WIT FQN for this standard leaf.
42    pub const fn fqn(self) -> &'static str {
43        match self {
44            Self::Felt => FELT_FQN,
45            Self::Word => WORD_FQN,
46            Self::AccountId => ACCOUNT_ID_FQN,
47            Self::AssetAmount => ASSET_AMOUNT_FQN,
48        }
49    }
50
51    /// Classifies a canonical WIT FQN as a standard leaf.
52    pub fn from_fqn(fqn: &str) -> Option<Self> {
53        Self::ALL.into_iter().find(|leaf| leaf.fqn() == fqn)
54    }
55}
56
57/// Parses, displays, and validates one fully-qualified WIT leaf type.
58pub trait ConsumerTypeCodec: Send + Sync {
59    /// Parses a string into its structural felt representation.
60    fn parse(&self, value: &str) -> Result<Vec<Felt>>;
61
62    /// Displays a structurally valid felt representation.
63    fn display(&self, felts: &[Felt]) -> Result<String>;
64
65    /// Validates the felt representation and any semantic type constraints.
66    fn validate(&self, felts: &[Felt]) -> Result<()>;
67}
68
69/// A registry of codecs keyed by canonical WIT fully-qualified type name.
70///
71/// The canonical form is `<namespace>:<package>/<interface>@<version>.<type>`. The version follows
72/// the interface, matching WIT interface identifiers. When a package has no version, the
73/// `@<version>` part is omitted, for example `miden:base/core-types.account-id`.
74/// The standard account ID codec displays the canonical mainnet bech32 form because the network
75/// identifier is not part of an account ID's felt representation.
76#[derive(Clone)]
77pub struct CodecRegistry {
78    codecs: BTreeMap<String, Arc<dyn ConsumerTypeCodec>>,
79}
80
81impl CodecRegistry {
82    /// Creates an empty codec registry.
83    pub fn empty() -> Self {
84        Self {
85            codecs: BTreeMap::new(),
86        }
87    }
88
89    /// Creates a registry containing all standard note storage codecs.
90    pub fn with_standard_codecs() -> Self {
91        let mut registry = Self::empty();
92        for leaf in StandardLeaf::ALL {
93            match leaf {
94                StandardLeaf::Felt => registry.register(leaf.fqn(), FeltCodec),
95                StandardLeaf::Word => registry.register(leaf.fqn(), WordCodec),
96                StandardLeaf::AccountId => registry.register(leaf.fqn(), AccountIdCodec),
97                StandardLeaf::AssetAmount => registry.register(leaf.fqn(), AssetAmountCodec),
98            }
99        }
100        registry
101    }
102
103    /// Registers or replaces a codec under its canonical WIT FQN.
104    pub fn register(&mut self, fqn: impl Into<String>, codec: impl ConsumerTypeCodec + 'static) {
105        self.register_shared(fqn, Arc::new(codec));
106    }
107
108    /// Registers or replaces a shared codec under its canonical WIT FQN.
109    pub fn register_shared(&mut self, fqn: impl Into<String>, codec: Arc<dyn ConsumerTypeCodec>) {
110        self.codecs.insert(fqn.into(), codec);
111    }
112
113    /// Returns the codec registered for a canonical WIT FQN.
114    pub fn codec(&self, fqn: &str) -> Option<&dyn ConsumerTypeCodec> {
115        self.codecs.get(fqn).map(Arc::as_ref)
116    }
117
118    /// Returns true when the canonical WIT FQN has a codec.
119    pub fn contains(&self, fqn: &str) -> bool {
120        self.codecs.contains_key(fqn)
121    }
122}
123
124impl Default for CodecRegistry {
125    fn default() -> Self {
126        Self::with_standard_codecs()
127    }
128}
129
130/// Parses and displays one field element.
131struct FeltCodec;
132
133impl ConsumerTypeCodec for FeltCodec {
134    fn parse(&self, value: &str) -> Result<Vec<Felt>> {
135        let felt = parse_felt(value)?;
136        Ok(write_repr(&felt))
137    }
138
139    fn display(&self, felts: &[Felt]) -> Result<String> {
140        read_repr::<Felt>(felts).map(|felt| felt.as_canonical_u64().to_string())
141    }
142
143    fn validate(&self, felts: &[Felt]) -> Result<()> {
144        read_repr::<Felt>(felts).map(|_| ())
145    }
146}
147
148/// Parses and displays a four-felt word.
149struct WordCodec;
150
151impl ConsumerTypeCodec for WordCodec {
152    fn parse(&self, value: &str) -> Result<Vec<Felt>> {
153        let value = value.trim();
154        let word = if value.starts_with("0x") || value.starts_with("0X") {
155            Word::parse(value).map_err(|err| Error::new(format!("invalid word hex: {err}")))?
156        } else {
157            let values = value.trim_matches(['[', ']']);
158            let felts = values.split(',').map(parse_felt).collect::<Result<Vec<_>>>()?;
159            let elements: [Felt; 4] = felts.try_into().map_err(|felts: Vec<Felt>| {
160                Error::new(format!(
161                    "a word needs four comma-separated felts, found {}",
162                    felts.len()
163                ))
164            })?;
165            Word::new(elements)
166        };
167        Ok(write_repr(&word))
168    }
169
170    fn display(&self, felts: &[Felt]) -> Result<String> {
171        read_repr::<Word>(felts).map(|word| word.to_hex())
172    }
173
174    fn validate(&self, felts: &[Felt]) -> Result<()> {
175        read_repr::<Word>(felts).map(|_| ())
176    }
177}
178
179/// Parses and displays a protocol account ID.
180struct AccountIdCodec;
181
182impl ConsumerTypeCodec for AccountIdCodec {
183    fn parse(&self, value: &str) -> Result<Vec<Felt>> {
184        let (account_id, _) = AccountId::parse(value)
185            .map_err(|err| Error::new(format!("invalid account-id: {err}")))?;
186        let mut felts = Vec::with_capacity(2);
187        let mut writer = FeltWriter::new(&mut felts);
188        writer.write(account_id.prefix().as_felt());
189        writer.write(account_id.suffix());
190        Ok(felts)
191    }
192
193    fn display(&self, felts: &[Felt]) -> Result<String> {
194        read_account_id(felts).map(|account_id| account_id.to_bech32(NetworkId::Mainnet))
195    }
196
197    fn validate(&self, felts: &[Felt]) -> Result<()> {
198        read_account_id(felts).map(|_| ())
199    }
200}
201
202/// Parses and displays a validated asset amount.
203struct AssetAmountCodec;
204
205impl ConsumerTypeCodec for AssetAmountCodec {
206    fn parse(&self, value: &str) -> Result<Vec<Felt>> {
207        let amount = value
208            .trim()
209            .parse::<u64>()
210            .map_err(|err| Error::new(format!("invalid asset amount: {err}")))?;
211        let amount = AssetAmount::new(amount)
212            .map_err(|err| Error::new(format!("invalid asset amount: {err}")))?;
213        let felt = Felt::from(amount);
214        Ok(write_repr(&felt))
215    }
216
217    fn display(&self, felts: &[Felt]) -> Result<String> {
218        read_asset_amount(felts).map(|amount| amount.to_string())
219    }
220
221    fn validate(&self, felts: &[Felt]) -> Result<()> {
222        read_asset_amount(felts).map(|_| ())
223    }
224}
225
226/// Parses a canonical felt from decimal or hexadecimal text.
227pub(crate) fn parse_felt(value: &str) -> Result<Felt> {
228    let value = parse_unsigned(value, "felt")?;
229    Felt::new(value).map_err(|err| Error::new(format!("invalid felt: {err}")))
230}
231
232/// Parses a decimal or hexadecimal unsigned integer.
233pub(crate) fn parse_unsigned(value: &str, ty: &str) -> Result<u64> {
234    let value = value.trim();
235    let parsed = match value.strip_prefix("0x").or_else(|| value.strip_prefix("0X")) {
236        Some(digits) => u64::from_str_radix(digits, 16),
237        None => value.parse::<u64>(),
238    };
239    parsed.map_err(|err| Error::new(format!("invalid {ty}: {err}")))
240}
241
242/// Encodes a felt-repr value through the shared writer.
243pub(crate) fn write_repr(value: &impl ToFeltRepr) -> Vec<Felt> {
244    let mut felts = Vec::new();
245    value.write_felt_repr(&mut FeltWriter::new(&mut felts));
246    felts
247}
248
249/// Decodes one felt-repr value and rejects trailing elements.
250fn read_repr<T: FromFeltRepr>(felts: &[Felt]) -> Result<T> {
251    let mut reader = FeltReader::new(felts);
252    let value = T::from_felt_repr(&mut reader)
253        .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?;
254    reader
255        .ensure_eof()
256        .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?;
257    Ok(value)
258}
259
260/// Decodes and validates an account ID in WIT record field order.
261fn read_account_id(felts: &[Felt]) -> Result<AccountId> {
262    let mut reader = FeltReader::new(felts);
263    let prefix = reader
264        .read()
265        .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?;
266    let suffix = reader
267        .read()
268        .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?;
269    reader
270        .ensure_eof()
271        .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?;
272    // WIT declares prefix before suffix, but the constructor takes suffix first.
273    AccountId::try_from_elements(suffix, prefix)
274        .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))
275}
276
277/// Decodes and validates an asset amount.
278fn read_asset_amount(felts: &[Felt]) -> Result<AssetAmount> {
279    let felt = read_repr::<Felt>(felts)?;
280    AssetAmount::try_from(felt)
281        .map_err(|err| Error::new(format!("invalid asset amount representation: {err}")))
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    /// Returns a valid account ID and its mainnet bech32 form.
289    fn account_id() -> (AccountId, String) {
290        let account_id =
291            AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap();
292        let bech32 = account_id.to_bech32(NetworkId::Mainnet);
293        (account_id, bech32)
294    }
295
296    #[test]
297    fn standard_registry_contains_canonical_versioned_fqns() {
298        let registry = CodecRegistry::default();
299
300        for leaf in StandardLeaf::ALL {
301            assert!(registry.contains(leaf.fqn()));
302        }
303        assert!(!CodecRegistry::empty().contains(FELT_FQN));
304    }
305
306    #[test]
307    fn standard_leaf_definition_is_pinned() {
308        assert_eq!(
309            StandardLeaf::ALL.map(StandardLeaf::fqn),
310            [FELT_FQN, WORD_FQN, ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN]
311        );
312        for leaf in StandardLeaf::ALL {
313            assert_eq!(StandardLeaf::from_fqn(leaf.fqn()), Some(leaf));
314            assert!(CodecRegistry::default().contains(leaf.fqn()));
315        }
316        assert_eq!(StandardLeaf::from_fqn("miden:base/core-types@1.0.0.digest"), None);
317    }
318
319    #[test]
320    fn felt_codec_accepts_decimal_and_hex() {
321        let codec = FeltCodec;
322
323        assert_eq!(codec.parse("42").unwrap(), codec.parse("0x2a").unwrap());
324        assert_eq!(codec.display(&codec.parse("42").unwrap()).unwrap(), "42");
325    }
326
327    #[test]
328    fn word_codec_accepts_hex_and_four_felts() {
329        let codec = WordCodec;
330        let from_felts = codec.parse("[1, 2, 3, 4]").unwrap();
331        let from_hex = codec.parse(&codec.display(&from_felts).unwrap()).unwrap();
332
333        assert_eq!(from_hex, from_felts);
334    }
335
336    #[test]
337    fn account_id_codec_uses_prefix_suffix_field_order() {
338        let codec = AccountIdCodec;
339        let (account_id, bech32) = account_id();
340        let felts = codec.parse(&bech32).unwrap();
341        let hex_felts = codec.parse(&account_id.to_hex()).unwrap();
342
343        assert_eq!(felts, [account_id.prefix().as_felt(), account_id.suffix()]);
344        assert_eq!(hex_felts, felts);
345        assert_eq!(codec.display(&felts).unwrap(), bech32);
346    }
347
348    #[test]
349    fn asset_amount_codec_enforces_protocol_limit() {
350        let codec = AssetAmountCodec;
351
352        assert!(codec.parse(&AssetAmount::MAX.as_u64().to_string()).is_ok());
353        assert!(codec.parse(&(AssetAmount::MAX.as_u64() + 1).to_string()).is_err());
354    }
355}