1use 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
11pub const FELT_FQN: &str = "miden:base/core-types@1.0.0.felt";
13pub const WORD_FQN: &str = "miden:base/core-types@1.0.0.word";
15pub const ACCOUNT_ID_FQN: &str = "miden:base/core-types@1.0.0.account-id";
17pub const ASSET_AMOUNT_FQN: &str = "miden:base/core-types@1.0.0.asset-amount";
19
20#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub enum StandardLeaf {
27 Felt,
29 Word,
31 AccountId,
33 AssetAmount,
35}
36
37impl StandardLeaf {
38 pub const ALL: [Self; 4] = [Self::Felt, Self::Word, Self::AccountId, Self::AssetAmount];
40
41 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 pub fn from_fqn(fqn: &str) -> Option<Self> {
53 Self::ALL.into_iter().find(|leaf| leaf.fqn() == fqn)
54 }
55}
56
57pub trait ConsumerTypeCodec: Send + Sync {
59 fn parse(&self, value: &str) -> Result<Vec<Felt>>;
61
62 fn display(&self, felts: &[Felt]) -> Result<String>;
64
65 fn validate(&self, felts: &[Felt]) -> Result<()>;
67}
68
69#[derive(Clone)]
77pub struct CodecRegistry {
78 codecs: BTreeMap<String, Arc<dyn ConsumerTypeCodec>>,
79}
80
81impl CodecRegistry {
82 pub fn empty() -> Self {
84 Self {
85 codecs: BTreeMap::new(),
86 }
87 }
88
89 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 pub fn register(&mut self, fqn: impl Into<String>, codec: impl ConsumerTypeCodec + 'static) {
105 self.register_shared(fqn, Arc::new(codec));
106 }
107
108 pub fn register_shared(&mut self, fqn: impl Into<String>, codec: Arc<dyn ConsumerTypeCodec>) {
110 self.codecs.insert(fqn.into(), codec);
111 }
112
113 pub fn codec(&self, fqn: &str) -> Option<&dyn ConsumerTypeCodec> {
115 self.codecs.get(fqn).map(Arc::as_ref)
116 }
117
118 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
130struct 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
148struct 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
179struct 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
202struct 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
226pub(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
232pub(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
242pub(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
249fn 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
260fn 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 AccountId::try_from_elements(suffix, prefix)
274 .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))
275}
276
277fn 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 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}