use std::{collections::BTreeMap, sync::Arc};
use miden_field::{Felt, Word};
use miden_field_repr::{FeltReader, FeltWriter, FromFeltRepr, ToFeltRepr};
use miden_protocol::{account::AccountId, address::NetworkId, asset::AssetAmount};
use crate::{Error, Result};
pub const FELT_FQN: &str = "miden:base/core-types@1.0.0.felt";
pub const WORD_FQN: &str = "miden:base/core-types@1.0.0.word";
pub const ACCOUNT_ID_FQN: &str = "miden:base/core-types@1.0.0.account-id";
pub const ASSET_AMOUNT_FQN: &str = "miden:base/core-types@1.0.0.asset-amount";
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum StandardLeaf {
Felt,
Word,
AccountId,
AssetAmount,
}
impl StandardLeaf {
pub const ALL: [Self; 4] = [Self::Felt, Self::Word, Self::AccountId, Self::AssetAmount];
pub const fn fqn(self) -> &'static str {
match self {
Self::Felt => FELT_FQN,
Self::Word => WORD_FQN,
Self::AccountId => ACCOUNT_ID_FQN,
Self::AssetAmount => ASSET_AMOUNT_FQN,
}
}
pub fn from_fqn(fqn: &str) -> Option<Self> {
Self::ALL.into_iter().find(|leaf| leaf.fqn() == fqn)
}
}
pub trait ConsumerTypeCodec: Send + Sync {
fn parse(&self, value: &str) -> Result<Vec<Felt>>;
fn display(&self, felts: &[Felt]) -> Result<String>;
fn validate(&self, felts: &[Felt]) -> Result<()>;
}
#[derive(Clone)]
pub struct CodecRegistry {
codecs: BTreeMap<String, Arc<dyn ConsumerTypeCodec>>,
}
impl CodecRegistry {
pub fn empty() -> Self {
Self {
codecs: BTreeMap::new(),
}
}
pub fn with_standard_codecs() -> Self {
let mut registry = Self::empty();
for leaf in StandardLeaf::ALL {
match leaf {
StandardLeaf::Felt => registry.register(leaf.fqn(), FeltCodec),
StandardLeaf::Word => registry.register(leaf.fqn(), WordCodec),
StandardLeaf::AccountId => registry.register(leaf.fqn(), AccountIdCodec),
StandardLeaf::AssetAmount => registry.register(leaf.fqn(), AssetAmountCodec),
}
}
registry
}
pub fn register(&mut self, fqn: impl Into<String>, codec: impl ConsumerTypeCodec + 'static) {
self.register_shared(fqn, Arc::new(codec));
}
pub fn register_shared(&mut self, fqn: impl Into<String>, codec: Arc<dyn ConsumerTypeCodec>) {
self.codecs.insert(fqn.into(), codec);
}
pub fn codec(&self, fqn: &str) -> Option<&dyn ConsumerTypeCodec> {
self.codecs.get(fqn).map(Arc::as_ref)
}
pub fn contains(&self, fqn: &str) -> bool {
self.codecs.contains_key(fqn)
}
}
impl Default for CodecRegistry {
fn default() -> Self {
Self::with_standard_codecs()
}
}
struct FeltCodec;
impl ConsumerTypeCodec for FeltCodec {
fn parse(&self, value: &str) -> Result<Vec<Felt>> {
let felt = parse_felt(value)?;
Ok(write_repr(&felt))
}
fn display(&self, felts: &[Felt]) -> Result<String> {
read_repr::<Felt>(felts).map(|felt| felt.as_canonical_u64().to_string())
}
fn validate(&self, felts: &[Felt]) -> Result<()> {
read_repr::<Felt>(felts).map(|_| ())
}
}
struct WordCodec;
impl ConsumerTypeCodec for WordCodec {
fn parse(&self, value: &str) -> Result<Vec<Felt>> {
let value = value.trim();
let word = if value.starts_with("0x") || value.starts_with("0X") {
Word::parse(value).map_err(|err| Error::new(format!("invalid word hex: {err}")))?
} else {
let values = value.trim_matches(['[', ']']);
let felts = values.split(',').map(parse_felt).collect::<Result<Vec<_>>>()?;
let elements: [Felt; 4] = felts.try_into().map_err(|felts: Vec<Felt>| {
Error::new(format!(
"a word needs four comma-separated felts, found {}",
felts.len()
))
})?;
Word::new(elements)
};
Ok(write_repr(&word))
}
fn display(&self, felts: &[Felt]) -> Result<String> {
read_repr::<Word>(felts).map(|word| word.to_hex())
}
fn validate(&self, felts: &[Felt]) -> Result<()> {
read_repr::<Word>(felts).map(|_| ())
}
}
struct AccountIdCodec;
impl ConsumerTypeCodec for AccountIdCodec {
fn parse(&self, value: &str) -> Result<Vec<Felt>> {
let (account_id, _) = AccountId::parse(value)
.map_err(|err| Error::new(format!("invalid account-id: {err}")))?;
let mut felts = Vec::with_capacity(2);
let mut writer = FeltWriter::new(&mut felts);
writer.write(account_id.prefix().as_felt());
writer.write(account_id.suffix());
Ok(felts)
}
fn display(&self, felts: &[Felt]) -> Result<String> {
read_account_id(felts).map(|account_id| account_id.to_bech32(NetworkId::Mainnet))
}
fn validate(&self, felts: &[Felt]) -> Result<()> {
read_account_id(felts).map(|_| ())
}
}
struct AssetAmountCodec;
impl ConsumerTypeCodec for AssetAmountCodec {
fn parse(&self, value: &str) -> Result<Vec<Felt>> {
let amount = value
.trim()
.parse::<u64>()
.map_err(|err| Error::new(format!("invalid asset amount: {err}")))?;
let amount = AssetAmount::new(amount)
.map_err(|err| Error::new(format!("invalid asset amount: {err}")))?;
let felt = Felt::from(amount);
Ok(write_repr(&felt))
}
fn display(&self, felts: &[Felt]) -> Result<String> {
read_asset_amount(felts).map(|amount| amount.to_string())
}
fn validate(&self, felts: &[Felt]) -> Result<()> {
read_asset_amount(felts).map(|_| ())
}
}
pub(crate) fn parse_felt(value: &str) -> Result<Felt> {
let value = parse_unsigned(value, "felt")?;
Felt::new(value).map_err(|err| Error::new(format!("invalid felt: {err}")))
}
pub(crate) fn parse_unsigned(value: &str, ty: &str) -> Result<u64> {
let value = value.trim();
let parsed = match value.strip_prefix("0x").or_else(|| value.strip_prefix("0X")) {
Some(digits) => u64::from_str_radix(digits, 16),
None => value.parse::<u64>(),
};
parsed.map_err(|err| Error::new(format!("invalid {ty}: {err}")))
}
pub(crate) fn write_repr(value: &impl ToFeltRepr) -> Vec<Felt> {
let mut felts = Vec::new();
value.write_felt_repr(&mut FeltWriter::new(&mut felts));
felts
}
fn read_repr<T: FromFeltRepr>(felts: &[Felt]) -> Result<T> {
let mut reader = FeltReader::new(felts);
let value = T::from_felt_repr(&mut reader)
.map_err(|err| Error::new(format!("invalid felt representation: {err}")))?;
reader
.ensure_eof()
.map_err(|err| Error::new(format!("invalid felt representation: {err}")))?;
Ok(value)
}
fn read_account_id(felts: &[Felt]) -> Result<AccountId> {
let mut reader = FeltReader::new(felts);
let prefix = reader
.read()
.map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?;
let suffix = reader
.read()
.map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?;
reader
.ensure_eof()
.map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?;
AccountId::try_from_elements(suffix, prefix)
.map_err(|err| Error::new(format!("invalid account-id representation: {err}")))
}
fn read_asset_amount(felts: &[Felt]) -> Result<AssetAmount> {
let felt = read_repr::<Felt>(felts)?;
AssetAmount::try_from(felt)
.map_err(|err| Error::new(format!("invalid asset amount representation: {err}")))
}
#[cfg(test)]
mod tests {
use super::*;
fn account_id() -> (AccountId, String) {
let account_id =
AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap();
let bech32 = account_id.to_bech32(NetworkId::Mainnet);
(account_id, bech32)
}
#[test]
fn standard_registry_contains_canonical_versioned_fqns() {
let registry = CodecRegistry::default();
for leaf in StandardLeaf::ALL {
assert!(registry.contains(leaf.fqn()));
}
assert!(!CodecRegistry::empty().contains(FELT_FQN));
}
#[test]
fn standard_leaf_definition_is_pinned() {
assert_eq!(
StandardLeaf::ALL.map(StandardLeaf::fqn),
[FELT_FQN, WORD_FQN, ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN]
);
for leaf in StandardLeaf::ALL {
assert_eq!(StandardLeaf::from_fqn(leaf.fqn()), Some(leaf));
assert!(CodecRegistry::default().contains(leaf.fqn()));
}
assert_eq!(StandardLeaf::from_fqn("miden:base/core-types@1.0.0.digest"), None);
}
#[test]
fn felt_codec_accepts_decimal_and_hex() {
let codec = FeltCodec;
assert_eq!(codec.parse("42").unwrap(), codec.parse("0x2a").unwrap());
assert_eq!(codec.display(&codec.parse("42").unwrap()).unwrap(), "42");
}
#[test]
fn word_codec_accepts_hex_and_four_felts() {
let codec = WordCodec;
let from_felts = codec.parse("[1, 2, 3, 4]").unwrap();
let from_hex = codec.parse(&codec.display(&from_felts).unwrap()).unwrap();
assert_eq!(from_hex, from_felts);
}
#[test]
fn account_id_codec_uses_prefix_suffix_field_order() {
let codec = AccountIdCodec;
let (account_id, bech32) = account_id();
let felts = codec.parse(&bech32).unwrap();
let hex_felts = codec.parse(&account_id.to_hex()).unwrap();
assert_eq!(felts, [account_id.prefix().as_felt(), account_id.suffix()]);
assert_eq!(hex_felts, felts);
assert_eq!(codec.display(&felts).unwrap(), bech32);
}
#[test]
fn asset_amount_codec_enforces_protocol_limit() {
let codec = AssetAmountCodec;
assert!(codec.parse(&AssetAmount::MAX.as_u64().to_string()).is_ok());
assert!(codec.parse(&(AssetAmount::MAX.as_u64() + 1).to_string()).is_err());
}
}