1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use core::error::Error;
4use core::fmt;
5
6pub use miden_objects::ConversionError;
7use miden_protocol::errors::NoteError;
8use miden_protocol::note::NoteId;
9use miden_protocol::utils::serde::DeserializationError;
10use thiserror::Error;
11
12use super::RpcEndpoint;
13
14pub mod node;
15pub use node::{AddTransactionError, EndpointError, RegisterAccountError};
16
17#[derive(Debug, Error)]
21pub enum RpcError {
22 #[error("accept header validation failed")]
23 AcceptHeaderError(#[from] AcceptHeaderError),
24 #[error("failed to connect to the Miden node")]
25 ConnectionError(#[source] Box<dyn Error + Send + Sync + 'static>),
26 #[error("failed to deserialize response from the Miden node: {0}")]
27 DeserializationError(String),
28 #[error("Miden node response is missing expected field '{0}'")]
29 ExpectedDataMissing(String),
30 #[error("rpc pagination error: {0}")]
31 PaginationError(String),
32 #[error("received an invalid response from the Miden node: {0}")]
33 InvalidResponse(String),
34 #[error("grpc request failed for {endpoint}: {error_kind}{}",
35 endpoint_error.as_ref().map_or(String::new(), |e| format!(" ({e})")))]
36 RequestError {
37 endpoint: RpcEndpoint,
38 error_kind: GrpcError,
39 endpoint_error: Option<EndpointError>,
40 #[source]
41 source: Option<Box<dyn Error + Send + Sync + 'static>>,
42 },
43 #[error("note {0} was not found on the Miden node")]
44 NoteNotFound(NoteId),
45 #[error("failed to seal transaction inputs for submission: {0}")]
46 TransactionInputsSealingFailed(String),
47 #[error("the transaction encryption key served by the node was rejected: {0}")]
48 TransactionEncryptionKeyRejected(String),
49 #[error("invalid Miden node endpoint '{0}'; expected format: https://host:port")]
50 InvalidNodeEndpoint(String),
51}
52
53impl RpcError {
54 pub fn endpoint_error(&self) -> Option<&EndpointError> {
56 match self {
57 Self::RequestError { endpoint_error, .. } => endpoint_error.as_ref(),
58 _ => None,
59 }
60 }
61
62 pub fn is_stale_transaction_encryption_key(&self) -> bool {
65 matches!(
66 self,
67 Self::RequestError {
68 endpoint: RpcEndpoint::SubmitProvenTx | RpcEndpoint::SubmitProvenBatch,
69 error_kind: GrpcError::FailedPrecondition,
70 ..
71 }
72 )
73 }
74
75 pub fn is_indeterminate_submission(&self) -> bool {
82 let Self::RequestError {
83 endpoint: RpcEndpoint::SubmitProvenTx | RpcEndpoint::SubmitProvenBatch,
84 error_kind,
85 ..
86 } = self
87 else {
88 return false;
89 };
90
91 !matches!(
92 error_kind,
93 GrpcError::InvalidArgument
95 | GrpcError::FailedPrecondition
96 | GrpcError::NotFound
97 | GrpcError::AlreadyExists
98 | GrpcError::OutOfRange
99 | GrpcError::ResourceExhausted
100 | GrpcError::Unauthenticated
101 | GrpcError::PermissionDenied
102 | GrpcError::Unimplemented
103 )
104 }
105}
106
107impl From<DeserializationError> for RpcError {
108 fn from(err: DeserializationError) -> Self {
109 Self::DeserializationError(err.to_string())
110 }
111}
112
113impl From<NoteError> for RpcError {
114 fn from(err: NoteError) -> Self {
115 Self::DeserializationError(err.to_string())
116 }
117}
118
119impl From<RpcConversionError> for RpcError {
120 fn from(err: RpcConversionError) -> Self {
121 Self::DeserializationError(err.to_string())
122 }
123}
124
125impl From<ConversionError> for RpcError {
126 fn from(err: ConversionError) -> Self {
127 Self::DeserializationError(err.to_string())
128 }
129}
130
131#[derive(Debug, Error)]
135pub enum RpcConversionError {
136 #[error("invalid field in node response: {0}")]
137 InvalidField(String),
138 #[error("field `{field_name}` expected to be present in protobuf representation of {entity}")]
139 MissingFieldInProtobufRepresentation {
140 entity: &'static str,
141 field_name: &'static str,
142 },
143 #[error("failed to convert a canonical object message: {0}")]
144 CanonicalConversion(#[from] ConversionError),
145}
146
147#[derive(Debug, Error)]
152pub enum GrpcError {
153 #[error("resource not found")]
154 NotFound,
155 #[error("invalid request parameters")]
156 InvalidArgument,
157 #[error("permission denied")]
158 PermissionDenied,
159 #[error("resource already exists")]
160 AlreadyExists,
161 #[error("request was rate-limited or the node's resources are exhausted; retry after a delay")]
162 ResourceExhausted,
163 #[error("precondition failed")]
164 FailedPrecondition,
165 #[error("operation was cancelled")]
166 Cancelled,
167 #[error("request to Miden node timed out; the node may be under heavy load")]
168 DeadlineExceeded,
169 #[error("Miden node is unavailable; check that the node is running and reachable")]
170 Unavailable,
171 #[error("Miden node returned an internal error; this is likely a node-side issue")]
172 Internal,
173 #[error("the requested method is not implemented by this version of the Miden node")]
174 Unimplemented,
175 #[error(
176 "request was rejected as unauthenticated; check your credentials and connection settings"
177 )]
178 Unauthenticated,
179 #[error("operation was aborted")]
180 Aborted,
181 #[error("operation was attempted past the valid range")]
182 OutOfRange,
183 #[error("unrecoverable data loss or corruption")]
184 DataLoss,
185 #[error("unknown error: {0}")]
186 Unknown(String),
187}
188
189impl GrpcError {
190 pub fn from_code(code: i32, message: Option<String>) -> Self {
193 match code {
194 1 => Self::Cancelled,
195 2 => Self::Unknown(message.unwrap_or_default()),
196 3 => Self::InvalidArgument,
197 4 => Self::DeadlineExceeded,
198 5 => Self::NotFound,
199 6 => Self::AlreadyExists,
200 7 => Self::PermissionDenied,
201 8 => Self::ResourceExhausted,
202 9 => Self::FailedPrecondition,
203 10 => Self::Aborted,
204 11 => Self::OutOfRange,
205 12 => Self::Unimplemented,
206 13 => Self::Internal,
207 14 => Self::Unavailable,
208 15 => Self::DataLoss,
209 16 => Self::Unauthenticated,
210 _ => Self::Unknown(
211 message.unwrap_or_else(|| format!("Unknown gRPC status code: {code}")),
212 ),
213 }
214 }
215}
216
217#[derive(Debug, Error)]
225pub enum AcceptHeaderError {
226 #[error("server rejected request - please check your version and network settings ({0})")]
227 NoSupportedMediaRange(AcceptHeaderContext),
228 #[error("server rejected request - parsing error: {0}")]
229 ParsingError(String),
230}
231
232#[derive(Debug, Clone)]
234pub struct AcceptHeaderContext {
235 pub client_version: String,
236 pub genesis_commitment: String,
237}
238
239impl fmt::Display for AcceptHeaderContext {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 write!(
242 f,
243 "client version: {}, genesis commitment: {}",
244 self.client_version, self.genesis_commitment
245 )
246 }
247}
248
249impl AcceptHeaderError {
250 pub fn try_from_message_with_context(
252 message: &str,
253 context: AcceptHeaderContext,
254 ) -> Option<Self> {
255 if message.contains(
257 "server does not support any of the specified application/vnd.miden content types",
258 ) {
259 return Some(Self::NoSupportedMediaRange(context));
260 }
261 if message.contains("genesis value failed to parse")
262 || message.contains("version value failed to parse")
263 {
264 return Some(Self::ParsingError(message.to_string()));
265 }
266 None
267 }
268}
269
270#[cfg(test)]
274mod tests {
275 use super::{GrpcError, RpcEndpoint, RpcError};
276
277 fn submission_failure(error_kind: GrpcError) -> RpcError {
278 RpcError::RequestError {
279 endpoint: RpcEndpoint::SubmitProvenTx,
280 error_kind,
281 endpoint_error: None,
282 source: None,
283 }
284 }
285
286 #[test]
289 fn transport_failures_are_indeterminate() {
290 for error_kind in [
291 GrpcError::Unavailable,
292 GrpcError::Unknown("transport error".into()),
293 GrpcError::Cancelled,
294 GrpcError::DeadlineExceeded,
295 GrpcError::Internal,
296 GrpcError::Aborted,
297 ] {
298 let label = format!("{error_kind:?}");
299 assert!(
300 submission_failure(error_kind).is_indeterminate_submission(),
301 "{label} must be treated as indeterminate"
302 );
303 }
304 }
305
306 #[test]
308 fn deliberate_rejections_are_definite() {
309 for error_kind in [
310 GrpcError::InvalidArgument,
311 GrpcError::FailedPrecondition,
312 GrpcError::ResourceExhausted,
313 GrpcError::NotFound,
314 GrpcError::AlreadyExists,
315 GrpcError::OutOfRange,
316 GrpcError::Unauthenticated,
317 GrpcError::PermissionDenied,
318 GrpcError::Unimplemented,
319 ] {
320 let label = format!("{error_kind:?}");
321 assert!(
322 !submission_failure(error_kind).is_indeterminate_submission(),
323 "{label} is a rejection, not an unknown outcome"
324 );
325 }
326 }
327
328 #[test]
330 fn reads_are_never_indeterminate_submissions() {
331 let err = RpcError::RequestError {
332 endpoint: RpcEndpoint::GetBlockHeaderByNumber,
333 error_kind: GrpcError::Unavailable,
334 endpoint_error: None,
335 source: None,
336 };
337
338 assert!(!err.is_indeterminate_submission());
339 }
340
341 #[test]
343 fn connection_errors_are_not_indeterminate() {
344 let err = RpcError::ConnectionError("no route to host".into());
345
346 assert!(!err.is_indeterminate_submission());
347 }
348}