Skip to main content

miden_client/rpc/errors/node/
mod.rs

1mod account;
2mod block;
3mod note;
4mod sync;
5mod transaction;
6
7pub use account::{GetAccountError, RegisterAccountError};
8pub use block::{GetBlockByNumberError, GetBlockHeaderError};
9pub use note::{GetNoteScriptByRootError, GetNotesByIdError};
10pub use sync::{
11    NoteSyncError,
12    SyncAccountStorageMapsError,
13    SyncAccountVaultError,
14    SyncChainMmrError,
15    SyncNullifiersError,
16    SyncTransactionsError,
17};
18use thiserror::Error;
19pub use transaction::AddTransactionError;
20
21use crate::rpc::RpcEndpoint;
22use crate::rpc::errors::GrpcError;
23
24/// Application-level error returned by the node for a specific RPC endpoint.
25///
26/// Each variant wraps a typed error parsed from the error code in the node's gRPC response.
27#[derive(Debug, Clone, PartialEq, Eq, Error)]
28pub enum EndpointError {
29    /// Error from the `SubmitProvenTransaction` or `SubmitProvenBatch` endpoint
30    #[error(transparent)]
31    AddTransaction(#[from] AddTransactionError),
32    /// Error from the `GetBlockHeaderByNumber` endpoint
33    #[error(transparent)]
34    GetBlockHeader(#[from] GetBlockHeaderError),
35    /// Error from the `GetBlockByNumber` endpoint
36    #[error(transparent)]
37    GetBlockByNumber(#[from] GetBlockByNumberError),
38    /// Error from the `SyncNotes` endpoint
39    #[error(transparent)]
40    NoteSync(#[from] NoteSyncError),
41    /// Error from the `SyncNullifiers` endpoint
42    #[error(transparent)]
43    SyncNullifiers(#[from] SyncNullifiersError),
44    /// Error from the `SyncAccountVault` endpoint
45    #[error(transparent)]
46    SyncAccountVault(#[from] SyncAccountVaultError),
47    /// Error from the `SyncStorageMaps` endpoint
48    #[error(transparent)]
49    SyncStorageMaps(#[from] SyncAccountStorageMapsError),
50    /// Error from the `SyncTransactions` endpoint
51    #[error(transparent)]
52    SyncTransactions(#[from] SyncTransactionsError),
53    /// Error from the `SyncChainMmr` endpoint
54    #[error(transparent)]
55    SyncChainMmr(#[from] SyncChainMmrError),
56    /// Error from the `GetNotesById` endpoint
57    #[error(transparent)]
58    GetNotesById(#[from] GetNotesByIdError),
59    /// Error from the `GetNoteScriptByRoot` endpoint
60    #[error(transparent)]
61    GetNoteScriptByRoot(#[from] GetNoteScriptByRootError),
62    /// Error from the `GetAccount` endpoint
63    #[error(transparent)]
64    GetAccount(#[from] GetAccountError),
65    /// Error from the `RegisterAccount` endpoint
66    #[error(transparent)]
67    RegisterAccount(#[from] RegisterAccountError),
68}
69
70/// Parses the application-level error code into a typed error for the given endpoint.
71///
72/// Returns `None` if details are empty or if the endpoint doesn't have typed errors.
73pub fn parse_node_error(
74    endpoint: &RpcEndpoint,
75    details: &[u8],
76    message: &str,
77) -> Option<EndpointError> {
78    let code = *details.first()?;
79
80    match endpoint {
81        RpcEndpoint::SubmitProvenTx | RpcEndpoint::SubmitProvenBatch => {
82            Some(EndpointError::AddTransaction(AddTransactionError::from_code(code, message)))
83        },
84        RpcEndpoint::GetBlockHeaderByNumber => {
85            Some(EndpointError::GetBlockHeader(GetBlockHeaderError::from_code(code, message)))
86        },
87        RpcEndpoint::GetBlockByNumber => {
88            Some(EndpointError::GetBlockByNumber(GetBlockByNumberError::from_code(code, message)))
89        },
90        RpcEndpoint::SyncNotes => {
91            Some(EndpointError::NoteSync(NoteSyncError::from_code(code, message)))
92        },
93        RpcEndpoint::SyncNullifiers => {
94            Some(EndpointError::SyncNullifiers(SyncNullifiersError::from_code(code, message)))
95        },
96        RpcEndpoint::SyncAccountVault => {
97            Some(EndpointError::SyncAccountVault(SyncAccountVaultError::from_code(code, message)))
98        },
99        RpcEndpoint::SyncStorageMaps => Some(EndpointError::SyncStorageMaps(
100            SyncAccountStorageMapsError::from_code(code, message),
101        )),
102        RpcEndpoint::SyncTransactions => {
103            Some(EndpointError::SyncTransactions(SyncTransactionsError::from_code(code, message)))
104        },
105        RpcEndpoint::GetNotesById => {
106            Some(EndpointError::GetNotesById(GetNotesByIdError::from_code(code, message)))
107        },
108        RpcEndpoint::GetNoteScriptByRoot => Some(EndpointError::GetNoteScriptByRoot(
109            GetNoteScriptByRootError::from_code(code, message),
110        )),
111        RpcEndpoint::GetAccount => {
112            Some(EndpointError::GetAccount(GetAccountError::from_code(code, message)))
113        },
114        RpcEndpoint::SyncChainMmr => {
115            Some(EndpointError::SyncChainMmr(SyncChainMmrError::from_code(code, message)))
116        },
117        // These endpoints don't have typed errors from the node
118        RpcEndpoint::Status
119        | RpcEndpoint::GetLimits
120        | RpcEndpoint::GetNetworkNoteStatus
121        | RpcEndpoint::GetTransactionEncryptionKey
122        | RpcEndpoint::RegisterAccount
123        | RpcEndpoint::IsAccountAllowed
124        | RpcEndpoint::IsInvitationCodeValid => None,
125    }
126}
127
128/// Parses the gRPC status code into a typed error for the given endpoint.
129pub fn parse_status_error(
130    endpoint: &RpcEndpoint,
131    error_kind: &GrpcError,
132    message: &str,
133) -> Option<EndpointError> {
134    // The match is exhaustive on purpose, so a new endpoint has to be classified before it
135    // compiles.
136    match endpoint {
137        RpcEndpoint::RegisterAccount => RegisterAccountError::from_grpc_error(error_kind, message)
138            .map(EndpointError::RegisterAccount),
139        RpcEndpoint::SubmitProvenTx
140        | RpcEndpoint::GetBlockHeaderByNumber
141        | RpcEndpoint::GetBlockByNumber
142        | RpcEndpoint::SyncNotes
143        | RpcEndpoint::SyncNullifiers
144        | RpcEndpoint::SyncAccountVault
145        | RpcEndpoint::SyncStorageMaps
146        | RpcEndpoint::SyncTransactions
147        | RpcEndpoint::GetNotesById
148        | RpcEndpoint::GetNoteScriptByRoot
149        | RpcEndpoint::GetAccount
150        | RpcEndpoint::SyncChainMmr
151        | RpcEndpoint::Status
152        | RpcEndpoint::GetLimits
153        | RpcEndpoint::GetNetworkNoteStatus
154        | RpcEndpoint::GetTransactionEncryptionKey
155        | RpcEndpoint::IsAccountAllowed
156        | RpcEndpoint::IsInvitationCodeValid
157        | RpcEndpoint::SubmitProvenBatch => None,
158    }
159}
160
161// TESTS
162// ================================================================================================
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    /// `RegisterAccount` is the only endpoint that classifies a status code into a typed error. The
169    /// other endpoints must return `None` for the same code, so a rejection from one of them is
170    /// never reported as a registration decision.
171    #[test]
172    fn only_the_register_account_endpoint_classifies_a_status_code() {
173        let registration =
174            parse_status_error(&RpcEndpoint::RegisterAccount, &GrpcError::NotFound, "unknown");
175
176        assert!(matches!(
177            registration,
178            Some(EndpointError::RegisterAccount(RegisterAccountError::InvitationNotFound))
179        ));
180
181        for endpoint in [
182            RpcEndpoint::GetAccount,
183            RpcEndpoint::SubmitProvenTx,
184            RpcEndpoint::GetNotesById,
185            RpcEndpoint::Status,
186        ] {
187            assert!(
188                parse_status_error(&endpoint, &GrpcError::NotFound, "unknown").is_none(),
189                "{endpoint:?} must not classify a status code"
190            );
191        }
192    }
193
194    /// A transport failure on the registration endpoint carries no decision about the code, so it
195    /// must stay unclassified.
196    #[test]
197    fn a_transport_failure_on_the_registration_endpoint_stays_unclassified() {
198        assert!(
199            parse_status_error(
200                &RpcEndpoint::RegisterAccount,
201                &GrpcError::Unavailable,
202                "node is down"
203            )
204            .is_none()
205        );
206    }
207
208    /// A request for a block after the chain tip must parse as `FutureBlock` on every sync
209    /// endpoint.
210    #[test]
211    fn a_block_after_the_chain_tip_parses_as_future_block() {
212        let message = "block_to (5) is greater than chain tip (4)";
213        assert_eq!(
214            parse_node_error(&RpcEndpoint::SyncNotes, &[2], message),
215            Some(EndpointError::NoteSync(NoteSyncError::FutureBlock))
216        );
217        assert_eq!(
218            parse_node_error(&RpcEndpoint::SyncChainMmr, &[2], message),
219            Some(EndpointError::SyncChainMmr(SyncChainMmrError::FutureBlock))
220        );
221        assert_eq!(
222            parse_node_error(&RpcEndpoint::SyncNullifiers, &[4], message),
223            Some(EndpointError::SyncNullifiers(SyncNullifiersError::FutureBlock))
224        );
225        assert_eq!(
226            parse_node_error(&RpcEndpoint::SyncAccountVault, &[4], message),
227            Some(EndpointError::SyncAccountVault(SyncAccountVaultError::FutureBlock))
228        );
229        assert_eq!(
230            parse_node_error(&RpcEndpoint::SyncStorageMaps, &[5], message),
231            Some(EndpointError::SyncStorageMaps(SyncAccountStorageMapsError::FutureBlock))
232        );
233        assert_eq!(
234            parse_node_error(&RpcEndpoint::SyncTransactions, &[5], message),
235            Some(EndpointError::SyncTransactions(SyncTransactionsError::FutureBlock))
236        );
237    }
238}