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