use alloc::boxed::Box;
use alloc::string::{String, ToString};
use core::error::Error;
use core::fmt;
use core::num::TryFromIntError;
use miden_protocol::account::AccountId;
use miden_protocol::crypto::merkle::MerkleError;
use miden_protocol::errors::NoteError;
use miden_protocol::note::NoteId;
use miden_protocol::utils::serde::DeserializationError;
use thiserror::Error;
use super::RpcEndpoint;
pub mod node;
pub use node::EndpointError;
#[derive(Debug, Error)]
pub enum RpcError {
#[error("accept header validation failed")]
AcceptHeaderError(#[from] AcceptHeaderError),
#[error(
"unexpected update received for private account {0}; private account state should not be sent by the node"
)]
AccountUpdateForPrivateAccountReceived(AccountId),
#[error("failed to connect to the Miden node")]
ConnectionError(#[source] Box<dyn Error + Send + Sync + 'static>),
#[error("failed to deserialize response from the Miden node: {0}")]
DeserializationError(String),
#[error("Miden node response is missing expected field '{0}'")]
ExpectedDataMissing(String),
#[error("rpc pagination error: {0}")]
PaginationError(String),
#[error("received an invalid response from the Miden node: {0}")]
InvalidResponse(String),
#[error("grpc request failed for {endpoint}: {error_kind}{}",
endpoint_error.as_ref().map_or(String::new(), |e| format!(" ({e})")))]
RequestError {
endpoint: RpcEndpoint,
error_kind: GrpcError,
endpoint_error: Option<EndpointError>,
#[source]
source: Option<Box<dyn Error + Send + Sync + 'static>>,
},
#[error("note {0} was not found on the Miden node")]
NoteNotFound(NoteId),
#[error("invalid Miden node endpoint '{0}'; expected format: https://host:port")]
InvalidNodeEndpoint(String),
}
impl RpcError {
pub fn endpoint_error(&self) -> Option<&EndpointError> {
match self {
Self::RequestError { endpoint_error, .. } => endpoint_error.as_ref(),
_ => None,
}
}
}
impl From<DeserializationError> for RpcError {
fn from(err: DeserializationError) -> Self {
Self::DeserializationError(err.to_string())
}
}
impl From<NoteError> for RpcError {
fn from(err: NoteError) -> Self {
Self::DeserializationError(err.to_string())
}
}
impl From<RpcConversionError> for RpcError {
fn from(err: RpcConversionError) -> Self {
Self::DeserializationError(err.to_string())
}
}
#[derive(Debug, Error)]
pub enum RpcConversionError {
#[error("failed to deserialize")]
DeserializationError(#[from] DeserializationError),
#[error(
"invalid field element: value is outside the valid range (0..modulus, where modulus = 2^64 - 2^32 + 1)"
)]
NotAValidFelt,
#[error("invalid note type in node response")]
NoteTypeError(#[from] NoteError),
#[error("merkle proof error in node response")]
MerkleError(#[from] MerkleError),
#[error("invalid field in node response: {0}")]
InvalidField(String),
#[error("integer conversion failed in node response")]
InvalidInt(#[from] TryFromIntError),
#[error("field `{field_name}` expected to be present in protobuf representation of {entity}")]
MissingFieldInProtobufRepresentation {
entity: &'static str,
field_name: &'static str,
},
}
#[derive(Debug, Error)]
pub enum GrpcError {
#[error("resource not found")]
NotFound,
#[error("invalid request parameters")]
InvalidArgument,
#[error("permission denied")]
PermissionDenied,
#[error("resource already exists")]
AlreadyExists,
#[error("request was rate-limited or the node's resources are exhausted; retry after a delay")]
ResourceExhausted,
#[error("precondition failed")]
FailedPrecondition,
#[error("operation was cancelled")]
Cancelled,
#[error("request to Miden node timed out; the node may be under heavy load")]
DeadlineExceeded,
#[error("Miden node is unavailable; check that the node is running and reachable")]
Unavailable,
#[error("Miden node returned an internal error; this is likely a node-side issue")]
Internal,
#[error("the requested method is not implemented by this version of the Miden node")]
Unimplemented,
#[error(
"request was rejected as unauthenticated; check your credentials and connection settings"
)]
Unauthenticated,
#[error("operation was aborted")]
Aborted,
#[error("operation was attempted past the valid range")]
OutOfRange,
#[error("unrecoverable data loss or corruption")]
DataLoss,
#[error("unknown error: {0}")]
Unknown(String),
}
impl GrpcError {
pub fn from_code(code: i32, message: Option<String>) -> Self {
match code {
1 => Self::Cancelled,
2 => Self::Unknown(message.unwrap_or_default()),
3 => Self::InvalidArgument,
4 => Self::DeadlineExceeded,
5 => Self::NotFound,
6 => Self::AlreadyExists,
7 => Self::PermissionDenied,
8 => Self::ResourceExhausted,
9 => Self::FailedPrecondition,
10 => Self::Aborted,
11 => Self::OutOfRange,
12 => Self::Unimplemented,
13 => Self::Internal,
14 => Self::Unavailable,
15 => Self::DataLoss,
16 => Self::Unauthenticated,
_ => Self::Unknown(
message.unwrap_or_else(|| format!("Unknown gRPC status code: {code}")),
),
}
}
}
#[derive(Debug, Error)]
pub enum AcceptHeaderError {
#[error("server rejected request - please check your version and network settings ({0})")]
NoSupportedMediaRange(AcceptHeaderContext),
#[error("server rejected request - parsing error: {0}")]
ParsingError(String),
}
#[derive(Debug, Clone)]
pub struct AcceptHeaderContext {
pub client_version: String,
pub genesis_commitment: String,
}
impl AcceptHeaderContext {
pub fn unknown() -> Self {
Self {
client_version: "unknown".to_string(),
genesis_commitment: "unknown".to_string(),
}
}
}
impl fmt::Display for AcceptHeaderContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"client version: {}, genesis commitment: {}",
self.client_version, self.genesis_commitment
)
}
}
impl AcceptHeaderError {
pub fn try_from_message_with_context(
message: &str,
context: AcceptHeaderContext,
) -> Option<Self> {
if message.contains(
"server does not support any of the specified application/vnd.miden content types",
) {
return Some(Self::NoSupportedMediaRange(context));
}
if message.contains("genesis value failed to parse")
|| message.contains("version value failed to parse")
{
return Some(Self::ParsingError(message.to_string()));
}
None
}
}