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