Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
29 changes: 29 additions & 0 deletions crates/rust-client/src/rpc/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ================================================================================================

Expand All @@ -36,6 +60,11 @@ pub enum RpcError {
#[source]
source: Option<Box<dyn Error + Send + Sync + 'static>>,
},
#[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}")]
Expand Down
34 changes: 33 additions & 1 deletion crates/rust-client/src/rpc/tonic_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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<dyn Error + Send + Sync + 'static>;

Expand All @@ -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.
/// <https://github.com/0xMiden/miden-node/pull/1266>
fn try_from(status: &Status) -> Result<Self, Self::Error> {
// 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;
Expand Down
Loading