miden-node-proto 0.14.7

Miden node message definitions (Store, Block Producer and RPC)
Documentation
use std::any::type_name;
use std::fmt;

// Re-export the GrpcError derive macro for convenience
pub use miden_node_grpc_error_macro::GrpcError;
use miden_protocol::utils::serde::DeserializationError;

#[cfg(test)]
mod test_macro;

// CONVERSION ERROR
// ================================================================================================

/// Opaque error for protobuf-to-domain conversions.
///
/// Captures an underlying error plus an optional field path stack that describes which nested
/// field caused the error (e.g. `"block.header.account_root: value is not in range 0..MODULUS"`).
///
/// Always maps to [`tonic::Status::invalid_argument()`].
#[derive(Debug)]
pub struct ConversionError {
    path: Vec<&'static str>,
    source: Box<dyn std::error::Error + Send + Sync>,
}

impl ConversionError {
    /// Create a new `ConversionError` wrapping any error source.
    pub fn new(source: impl std::error::Error + Send + Sync + 'static) -> Self {
        Self {
            path: Vec::new(),
            source: Box::new(source),
        }
    }

    /// Add field context to the error path.
    ///
    /// Called from inner to outer, so the path accumulates in reverse
    /// (outermost field pushed last).
    ///
    /// Use this to annotate errors from `try_into()` / `try_from()` where the underlying
    /// error has no knowledge of which field it originated from. Do not use it with
    /// [`missing_field`](Self::missing_field) which already embeds the field name in its
    /// message.
    #[must_use]
    pub fn context(mut self, field: &'static str) -> Self {
        self.path.push(field);
        self
    }

    /// Create a "missing field" error for a protobuf message type `T`.
    pub fn missing_field<T: prost::Message>(field_name: &'static str) -> Self {
        Self {
            path: Vec::new(),
            source: Box::new(MissingFieldError { entity: type_name::<T>(), field_name }),
        }
    }

    /// Create a deserialization error for a named entity.
    pub fn deserialization(entity: &'static str, source: DeserializationError) -> Self {
        Self {
            path: Vec::new(),
            source: Box::new(DeserializationErrorWrapper { entity, source }),
        }
    }

    /// Create a `ConversionError` from an ad-hoc error message.
    pub fn message(msg: impl Into<String>) -> Self {
        Self {
            path: Vec::new(),
            source: Box::new(StringError(msg.into())),
        }
    }
}

impl fmt::Display for ConversionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if !self.path.is_empty() {
            // Path was pushed inner-to-outer, so reverse for display.
            for (i, segment) in self.path.iter().rev().enumerate() {
                if i > 0 {
                    f.write_str(".")?;
                }
                f.write_str(segment)?;
            }
            f.write_str(": ")?;
        }
        write!(f, "{}", self.source)
    }
}

impl std::error::Error for ConversionError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&*self.source)
    }
}

impl From<ConversionError> for tonic::Status {
    fn from(value: ConversionError) -> Self {
        tonic::Status::invalid_argument(value.to_string())
    }
}

// INTERNAL HELPER ERROR TYPES
// ================================================================================================

#[derive(Debug)]
struct MissingFieldError {
    entity: &'static str,
    field_name: &'static str,
}

impl fmt::Display for MissingFieldError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "field `{}::{}` is missing", self.entity, self.field_name)
    }
}

impl std::error::Error for MissingFieldError {}

#[derive(Debug)]
struct DeserializationErrorWrapper {
    entity: &'static str,
    source: DeserializationError,
}

impl fmt::Display for DeserializationErrorWrapper {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "failed to deserialize {}: {}", self.entity, self.source)
    }
}

impl std::error::Error for DeserializationErrorWrapper {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.source)
    }
}

#[derive(Debug)]
struct StringError(String);

impl fmt::Display for StringError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl std::error::Error for StringError {}

// CONVERSION RESULT EXTENSION TRAIT
// ================================================================================================

/// Extension trait to ergonomically add field context to [`ConversionError`] results.
///
/// This makes it easy to inject field names into the error path at each `?` site:
///
/// ```rust,ignore
/// let account_root = value.account_root
///     .ok_or(ConversionError::missing_field::<proto::BlockHeader>("account_root"))?
///     .try_into()
///     .context("account_root")?;
/// ```
///
/// The context stacks automatically through nested conversions, producing error paths like
/// `"header.account_root: value is not in range 0..MODULUS"`.
pub trait ConversionResultExt<T> {
    /// Add field context to the error, wrapping it in a [`ConversionError`] if needed.
    fn context(self, field: &'static str) -> Result<T, ConversionError>;
}

impl<T, E: Into<ConversionError>> ConversionResultExt<T> for Result<T, E> {
    fn context(self, field: &'static str) -> Result<T, ConversionError> {
        self.map_err(|e| e.into().context(field))
    }
}

// FROM IMPLS FOR EXTERNAL ERROR TYPES
// ================================================================================================

macro_rules! impl_from_for_conversion_error {
    ($($ty:ty),* $(,)?) => {
        $(
            impl From<$ty> for ConversionError {
                fn from(e: $ty) -> Self {
                    Self::new(e)
                }
            }
        )*
    };
}

impl_from_for_conversion_error!(
    hex::FromHexError,
    miden_protocol::errors::AccountError,
    miden_protocol::errors::AssetError,
    miden_protocol::errors::AssetVaultError,
    miden_protocol::errors::FeeError,
    miden_protocol::errors::NoteError,
    miden_protocol::errors::StorageSlotNameError,
    miden_protocol::crypto::merkle::MerkleError,
    miden_protocol::crypto::merkle::smt::SmtLeafError,
    miden_protocol::crypto::merkle::smt::SmtProofError,
    miden_standards::note::NetworkAccountTargetError,
    std::num::TryFromIntError,
    DeserializationError,
);