diff --git a/CHANGELOG.md b/CHANGELOG.md index d758e11f1..17e722c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,13 @@ * [BREAKING] WebClient `AccountComponent.createAuthComponentFromCommitment` now takes `AuthScheme` (enum) instead of a numeric scheme id. The old `AccountComponent.createAuthComponent` method was removed; use `createAuthComponentFromSecretKey` instead ([#1578](https://github.com/0xMiden/miden-client/issues/1578)). * Changed `blockNum` type from `string` to `number` in WebClient transaction interfaces for better type safety and consistency ([#1528](https://github.com/0xMiden/miden-client/pull/1528)). * Consolidated `FetchedNote` fields into `NoteHeader` ([#1536](https://github.com/0xMiden/miden-client/pull/1536)). +* Added support for handling application-level gRPC errors from the node, exposing structured error codes and messages to clients ([#1564](https://github.com/0xMiden/miden-client/pull/1564)). * Tied the web client's IndexedDB schema to the running package version, automatically recreating or wiping stale stores and applying the same guard to `forceImportStore` ([#1576](https://github.com/0xMiden/miden-client/pull/1576)). * Added the `--remote-prover-timeout` configuration to the CLI ([#1551](https://github.com/0xMiden/miden-client/pull/1551)). * Surface WASM worker errors to the JS wrapper with their original stacks for clearer diagnostics ([#1565](https://github.com/0xMiden/miden-client/issues/1565)). +* Fixed a bug where insertions in the `Addresses` table in the IndexedDB Store resulted in the `id` and `address` fields being inverted with each other ([#1532](https://github.com/0xMiden/miden-client/pull/1532)). + +## 0.12.4 (TBD) * Added doc_cfg as top level cfg_attr to turn on feature annotations in docs.rs and added make targets to serve the docs ([#1543](https://github.com/0xMiden/miden-client/pull/1543)). * Updated `DataStore` implementation to prevent retrieving whole `vault` and `storage` ([#1419](https://github.com/0xMiden/miden-client/pull/1419)) * Added RPC limit handling for `sync_nullifiers` endpoint ([#1590](https://github.com/0xMiden/miden-client/pull/1590)). diff --git a/crates/rust-client/src/rpc/errors.rs b/crates/rust-client/src/rpc/errors.rs index 82d973835..16381be9a 100644 --- a/crates/rust-client/src/rpc/errors.rs +++ b/crates/rust-client/src/rpc/errors.rs @@ -12,6 +12,30 @@ use thiserror::Error; use super::NodeRpcClientEndpoint; +// APPLICATION-LEVEL ERROR +// ================================================================================================ + +/// Application-level error returned by the node in gRPC responses. +/// +/// These errors represent application-specific failures (e.g., transaction validation failures) +/// that are distinct from gRPC transport-level errors. The error includes a numeric code and +/// a human-readable message. +#[derive(Debug, Clone, PartialEq, Eq, Error)] +#[error("Application error (code: {code}): {message}")] +pub struct AppLevelError { + /// Numeric error code specific to the application. + pub code: u32, + /// Human-readable error message. + pub message: String, +} + +impl AppLevelError { + /// Creates a new application-level error. + pub fn new(code: u32, message: String) -> Self { + Self { code, message } + } +} + // RPC ERROR // ================================================================================================ @@ -36,6 +60,11 @@ pub enum RpcError { #[source] source: Option>, }, + #[error("application-level error for {endpoint}: {error}")] + AppLevelError { + endpoint: NodeRpcClientEndpoint, + error: AppLevelError, + }, #[error("note with id {0} was not found")] NoteNotFound(NoteId), #[error("invalid node endpoint: {0}")] diff --git a/crates/rust-client/src/rpc/tonic_client/mod.rs b/crates/rust-client/src/rpc/tonic_client/mod.rs index 4e84b823e..13ec0f5dd 100644 --- a/crates/rust-client/src/rpc/tonic_client/mod.rs +++ b/crates/rust-client/src/rpc/tonic_client/mod.rs @@ -3,6 +3,7 @@ use alloc::boxed::Box; use alloc::collections::{BTreeMap, BTreeSet}; use alloc::string::{String, ToString}; use alloc::vec::Vec; +use core::convert::TryFrom; use core::error::Error; use miden_objects::account::{Account, AccountCode, AccountId}; @@ -33,7 +34,7 @@ use super::{ use crate::rpc::domain::account_vault::AccountVaultInfo; use crate::rpc::domain::storage_map::StorageMapInfo; use crate::rpc::domain::transaction::TransactionsInfo; -use crate::rpc::errors::{AcceptHeaderError, GrpcError, RpcConversionError}; +use crate::rpc::errors::{AcceptHeaderError, AppLevelError, GrpcError, RpcConversionError}; use crate::rpc::generated::rpc::BlockRange; use crate::rpc::generated::rpc::account_proof_request::account_detail_request::StorageMapDetailRequest; use crate::rpc::{NOTE_IDS_LIMIT, NULLIFIER_PREFIXES_LIMIT, generated as proto}; @@ -625,6 +626,10 @@ impl RpcError { return Self::AcceptHeaderError(accept_error); } + if let Ok(app_error) = AppLevelError::try_from(&status) { + return Self::AppLevelError { endpoint, error: app_error }; + } + let error_kind = GrpcError::from(&status); let source = Box::new(status) as Box; @@ -642,6 +647,33 @@ impl From<&Status> for GrpcError { } } +impl TryFrom<&Status> for AppLevelError { + type Error = (); + + /// Attempts to extract application-level error information from a gRPC Status. + /// + /// Application-level errors are returned as `tonic::Status` errors with the error code encoded + /// in the Status details as a `u8` byte, and the error message in the Status message string. + /// + fn try_from(status: &Status) -> Result { + // The node encodes application-level errors as Status with: + // - Error code (u8) in Status details: `vec![api_error.api_code()].into()` + // - Error message in Status message (or "Internal error" for internal errors) + // - Status code (tonic::Code) determined by the error type + + let details = status.details(); + if details.is_empty() || details.len() != 1 { + return Err(()); + } + + // The error code is encoded as a single u8 byte in the details + let error_code = u32::from(details[0]); + let error_message = status.message().to_string(); + + Ok(AppLevelError::new(error_code, error_message)) + } +} + #[cfg(test)] mod tests { use std::boxed::Box;