Skip to main content

miden_client/rpc/
errors.rs

1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use core::error::Error;
4use core::num::TryFromIntError;
5
6use miden_protocol::account::AccountId;
7use miden_protocol::crypto::merkle::MerkleError;
8use miden_protocol::errors::NoteError;
9use miden_protocol::note::NoteId;
10use miden_protocol::utils::DeserializationError;
11use thiserror::Error;
12
13use super::NodeRpcClientEndpoint;
14
15// RPC ERROR
16// ================================================================================================
17
18#[derive(Debug, Error)]
19pub enum RpcError {
20    #[error("accept header validation failed")]
21    AcceptHeaderError(#[from] AcceptHeaderError),
22    #[error(
23        "unexpected update received for private account {0}; private account state should not be sent by the node"
24    )]
25    AccountUpdateForPrivateAccountReceived(AccountId),
26    #[error("failed to connect to the Miden node")]
27    ConnectionError(#[source] Box<dyn Error + Send + Sync + 'static>),
28    #[error("failed to deserialize response from the Miden node: {0}")]
29    DeserializationError(String),
30    #[error("Miden node response is missing expected field '{0}'")]
31    ExpectedDataMissing(String),
32    #[error("received an invalid response from the Miden node: {0}")]
33    InvalidResponse(String),
34    #[error("request to Miden node failed ({endpoint}): {error_kind}")]
35    GrpcError {
36        endpoint: NodeRpcClientEndpoint,
37        error_kind: GrpcError,
38        #[source]
39        source: Option<Box<dyn Error + Send + Sync + 'static>>,
40    },
41    #[error("note {0} was not found on the Miden node")]
42    NoteNotFound(NoteId),
43    #[error("invalid Miden node endpoint '{0}'; expected format: https://host:port")]
44    InvalidNodeEndpoint(String),
45}
46
47impl From<DeserializationError> for RpcError {
48    fn from(err: DeserializationError) -> Self {
49        Self::DeserializationError(err.to_string())
50    }
51}
52
53impl From<NoteError> for RpcError {
54    fn from(err: NoteError) -> Self {
55        Self::DeserializationError(err.to_string())
56    }
57}
58
59impl From<RpcConversionError> for RpcError {
60    fn from(err: RpcConversionError) -> Self {
61        Self::DeserializationError(err.to_string())
62    }
63}
64
65// RPC CONVERSION ERROR
66// ================================================================================================
67
68#[derive(Debug, Error)]
69pub enum RpcConversionError {
70    #[error("failed to deserialize")]
71    DeserializationError(#[from] DeserializationError),
72    #[error(
73        "invalid field element: value is outside the valid range (0..modulus, where modulus = 2^64 - 2^32 + 1)"
74    )]
75    NotAValidFelt,
76    #[error("invalid note type in node response")]
77    NoteTypeError(#[from] NoteError),
78    #[error("merkle proof error in node response")]
79    MerkleError(#[from] MerkleError),
80    #[error("invalid field in node response: {0}")]
81    InvalidField(String),
82    #[error("integer conversion failed in node response")]
83    InvalidInt(#[from] TryFromIntError),
84    #[error("field `{field_name}` expected to be present in protobuf representation of {entity}")]
85    MissingFieldInProtobufRepresentation {
86        entity: &'static str,
87        field_name: &'static str,
88    },
89}
90
91// GRPC ERROR KIND
92// ================================================================================================
93
94/// Categorizes gRPC errors based on their status codes and common patterns
95#[derive(Debug, Error)]
96pub enum GrpcError {
97    #[error("resource not found")]
98    NotFound,
99    #[error("invalid request parameters")]
100    InvalidArgument,
101    #[error("permission denied")]
102    PermissionDenied,
103    #[error("resource already exists")]
104    AlreadyExists,
105    #[error("request was rate-limited or the node's resources are exhausted; retry after a delay")]
106    ResourceExhausted,
107    #[error("precondition failed")]
108    FailedPrecondition,
109    #[error("operation was cancelled")]
110    Cancelled,
111    #[error("request to Miden node timed out; the node may be under heavy load")]
112    DeadlineExceeded,
113    #[error("Miden node is unavailable; check that the node is running and reachable")]
114    Unavailable,
115    #[error("Miden node returned an internal error; this is likely a node-side issue")]
116    Internal,
117    #[error("the requested method is not implemented by this version of the Miden node")]
118    Unimplemented,
119    #[error(
120        "request was rejected as unauthenticated; check your credentials and connection settings"
121    )]
122    Unauthenticated,
123    #[error("operation was aborted")]
124    Aborted,
125    #[error("operation was attempted past the valid range")]
126    OutOfRange,
127    #[error("unrecoverable data loss or corruption")]
128    DataLoss,
129    #[error("unknown error: {0}")]
130    Unknown(String),
131}
132
133impl GrpcError {
134    /// Creates a `GrpcError` from a gRPC status code following the official specification
135    /// <https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc>
136    pub fn from_code(code: i32, message: Option<String>) -> Self {
137        match code {
138            1 => Self::Cancelled,
139            2 => Self::Unknown(message.unwrap_or_default()),
140            3 => Self::InvalidArgument,
141            4 => Self::DeadlineExceeded,
142            5 => Self::NotFound,
143            6 => Self::AlreadyExists,
144            7 => Self::PermissionDenied,
145            8 => Self::ResourceExhausted,
146            9 => Self::FailedPrecondition,
147            10 => Self::Aborted,
148            11 => Self::OutOfRange,
149            12 => Self::Unimplemented,
150            13 => Self::Internal,
151            14 => Self::Unavailable,
152            15 => Self::DataLoss,
153            16 => Self::Unauthenticated,
154            _ => Self::Unknown(
155                message.unwrap_or_else(|| format!("Unknown gRPC status code: {code}")),
156            ),
157        }
158    }
159}
160
161// ACCEPT HEADER ERROR
162// ================================================================================================
163
164// TODO: Once the node returns structure error information, replace this with a more structure
165// approach. See miden-client/#1129 for more information.
166
167/// Errors that can occur during accept header validation.
168#[derive(Debug, Error)]
169pub enum AcceptHeaderError {
170    #[error("server rejected request - please check your version and network settings")]
171    NoSupportedMediaRange,
172    #[error("server rejected request - parsing error: {0}")]
173    ParsingError(String),
174}
175
176impl AcceptHeaderError {
177    /// Try to parse an accept header error from a message string
178    pub fn try_from_message(message: &str) -> Option<Self> {
179        // Check for the main compatibility error message
180        if message.contains(
181            "server does not support any of the specified application/vnd.miden content types",
182        ) {
183            return Some(Self::NoSupportedMediaRange);
184        }
185        if message.contains("genesis value failed to parse")
186            || message.contains("version value failed to parse")
187        {
188            return Some(Self::ParsingError(message.to_string()));
189        }
190        None
191    }
192}