Skip to main content

miden_node_proto/errors/
mod.rs

1use std::any::type_name;
2use std::fmt;
3
4// Re-export the GrpcError derive macro for convenience
5pub use miden_node_grpc_error_macro::GrpcError;
6use miden_protocol::utils::serde::DeserializationError;
7
8#[cfg(test)]
9mod test_macro;
10
11// CONVERSION ERROR
12// ================================================================================================
13
14/// Opaque error for protobuf-to-domain conversions.
15///
16/// Captures an underlying error plus an optional field path stack that describes which nested
17/// field caused the error (e.g. `"block.header.account_root: value is not in range 0..MODULUS"`).
18///
19/// Always maps to [`tonic::Status::invalid_argument()`].
20#[derive(Debug)]
21pub struct ConversionError {
22    path: Vec<&'static str>,
23    source: Box<dyn std::error::Error + Send + Sync>,
24}
25
26impl ConversionError {
27    /// Create a new `ConversionError` wrapping any error source.
28    pub fn new(source: impl std::error::Error + Send + Sync + 'static) -> Self {
29        Self {
30            path: Vec::new(),
31            source: Box::new(source),
32        }
33    }
34
35    /// Add field context to the error path.
36    ///
37    /// Called from inner to outer, so the path accumulates in reverse
38    /// (outermost field pushed last).
39    ///
40    /// Use this to annotate errors from `try_into()` / `try_from()` where the underlying
41    /// error has no knowledge of which field it originated from. Do not use it with
42    /// [`missing_field`](Self::missing_field) which already embeds the field name in its
43    /// message.
44    #[must_use]
45    pub fn context(mut self, field: &'static str) -> Self {
46        self.path.push(field);
47        self
48    }
49
50    /// Create a "missing field" error for a protobuf message type `T`.
51    pub fn missing_field<T: prost::Message>(field_name: &'static str) -> Self {
52        Self {
53            path: Vec::new(),
54            source: Box::new(MissingFieldError { entity: type_name::<T>(), field_name }),
55        }
56    }
57
58    /// Create a deserialization error for a named entity.
59    pub fn deserialization(entity: &'static str, source: DeserializationError) -> Self {
60        Self {
61            path: Vec::new(),
62            source: Box::new(DeserializationErrorWrapper { entity, source }),
63        }
64    }
65
66    /// Create a `ConversionError` from an ad-hoc error message.
67    pub fn message(msg: impl Into<String>) -> Self {
68        Self {
69            path: Vec::new(),
70            source: Box::new(StringError(msg.into())),
71        }
72    }
73}
74
75impl fmt::Display for ConversionError {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        if !self.path.is_empty() {
78            // Path was pushed inner-to-outer, so reverse for display.
79            for (i, segment) in self.path.iter().rev().enumerate() {
80                if i > 0 {
81                    f.write_str(".")?;
82                }
83                f.write_str(segment)?;
84            }
85            f.write_str(": ")?;
86        }
87        write!(f, "{}", self.source)
88    }
89}
90
91impl std::error::Error for ConversionError {
92    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
93        Some(&*self.source)
94    }
95}
96
97impl From<ConversionError> for tonic::Status {
98    fn from(value: ConversionError) -> Self {
99        tonic::Status::invalid_argument(value.to_string())
100    }
101}
102
103// INTERNAL HELPER ERROR TYPES
104// ================================================================================================
105
106#[derive(Debug)]
107struct MissingFieldError {
108    entity: &'static str,
109    field_name: &'static str,
110}
111
112impl fmt::Display for MissingFieldError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "field `{}::{}` is missing", self.entity, self.field_name)
115    }
116}
117
118impl std::error::Error for MissingFieldError {}
119
120#[derive(Debug)]
121struct DeserializationErrorWrapper {
122    entity: &'static str,
123    source: DeserializationError,
124}
125
126impl fmt::Display for DeserializationErrorWrapper {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(f, "failed to deserialize {}: {}", self.entity, self.source)
129    }
130}
131
132impl std::error::Error for DeserializationErrorWrapper {
133    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
134        Some(&self.source)
135    }
136}
137
138#[derive(Debug)]
139struct StringError(String);
140
141impl fmt::Display for StringError {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.write_str(&self.0)
144    }
145}
146
147impl std::error::Error for StringError {}
148
149// CONVERSION RESULT EXTENSION TRAIT
150// ================================================================================================
151
152/// Extension trait to ergonomically add field context to [`ConversionError`] results.
153///
154/// This makes it easy to inject field names into the error path at each `?` site:
155///
156/// ```rust,ignore
157/// let account_root = value.account_root
158///     .ok_or(ConversionError::missing_field::<proto::BlockHeader>("account_root"))?
159///     .try_into()
160///     .context("account_root")?;
161/// ```
162///
163/// The context stacks automatically through nested conversions, producing error paths like
164/// `"header.account_root: value is not in range 0..MODULUS"`.
165pub trait ConversionResultExt<T> {
166    /// Add field context to the error, wrapping it in a [`ConversionError`] if needed.
167    fn context(self, field: &'static str) -> Result<T, ConversionError>;
168}
169
170impl<T, E: Into<ConversionError>> ConversionResultExt<T> for Result<T, E> {
171    fn context(self, field: &'static str) -> Result<T, ConversionError> {
172        self.map_err(|e| e.into().context(field))
173    }
174}
175
176// FROM IMPLS FOR EXTERNAL ERROR TYPES
177// ================================================================================================
178
179macro_rules! impl_from_for_conversion_error {
180    ($($ty:ty),* $(,)?) => {
181        $(
182            impl From<$ty> for ConversionError {
183                fn from(e: $ty) -> Self {
184                    Self::new(e)
185                }
186            }
187        )*
188    };
189}
190
191impl_from_for_conversion_error!(
192    hex::FromHexError,
193    miden_protocol::errors::AccountError,
194    miden_protocol::errors::AssetError,
195    miden_protocol::errors::AssetVaultError,
196    miden_protocol::errors::FeeError,
197    miden_protocol::errors::NoteError,
198    miden_protocol::errors::StorageSlotNameError,
199    miden_protocol::crypto::merkle::MerkleError,
200    miden_protocol::crypto::merkle::smt::SmtLeafError,
201    miden_protocol::crypto::merkle::smt::SmtProofError,
202    miden_standards::note::NetworkAccountTargetError,
203    std::num::TryFromIntError,
204    DeserializationError,
205);