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 => None,
124    }
125}
126
127/// Parses the gRPC status code into a typed error for the given endpoint.
128pub fn parse_status_error(
129    endpoint: &RpcEndpoint,
130    error_kind: &GrpcError,
131    message: &str,
132) -> Option<EndpointError> {
133    // The match is exhaustive on purpose, so a new endpoint has to be classified before it
134    // compiles.
135    match endpoint {
136        RpcEndpoint::RegisterAccount => RegisterAccountError::from_grpc_error(error_kind, message)
137            .map(EndpointError::RegisterAccount),
138        RpcEndpoint::SubmitProvenTx
139        | RpcEndpoint::GetBlockHeaderByNumber
140        | RpcEndpoint::GetBlockByNumber
141        | RpcEndpoint::SyncNotes
142        | RpcEndpoint::SyncNullifiers
143        | RpcEndpoint::SyncAccountVault
144        | RpcEndpoint::SyncStorageMaps
145        | RpcEndpoint::SyncTransactions
146        | RpcEndpoint::GetNotesById
147        | RpcEndpoint::GetNoteScriptByRoot
148        | RpcEndpoint::GetAccount
149        | RpcEndpoint::SyncChainMmr
150        | RpcEndpoint::Status
151        | RpcEndpoint::GetLimits
152        | RpcEndpoint::GetNetworkNoteStatus
153        | RpcEndpoint::GetTransactionEncryptionKey
154        | RpcEndpoint::IsAccountAllowed
155        | RpcEndpoint::SubmitProvenBatch => None,
156    }
157}
158
159// TESTS
160// ================================================================================================
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    /// `RegisterAccount` is the only endpoint that classifies a status code into a typed error. The
167    /// other endpoints must return `None` for the same code, so a rejection from one of them is
168    /// never reported as a registration decision.
169    #[test]
170    fn only_the_register_account_endpoint_classifies_a_status_code() {
171        let registration =
172            parse_status_error(&RpcEndpoint::RegisterAccount, &GrpcError::NotFound, "unknown");
173
174        assert!(matches!(
175            registration,
176            Some(EndpointError::RegisterAccount(RegisterAccountError::InvitationNotFound))
177        ));
178
179        for endpoint in [
180            RpcEndpoint::GetAccount,
181            RpcEndpoint::SubmitProvenTx,
182            RpcEndpoint::GetNotesById,
183            RpcEndpoint::Status,
184        ] {
185            assert!(
186                parse_status_error(&endpoint, &GrpcError::NotFound, "unknown").is_none(),
187                "{endpoint:?} must not classify a status code"
188            );
189        }
190    }
191
192    /// A transport failure on the registration endpoint carries no decision about the code, so it
193    /// must stay unclassified.
194    #[test]
195    fn a_transport_failure_on_the_registration_endpoint_stays_unclassified() {
196        assert!(
197            parse_status_error(
198                &RpcEndpoint::RegisterAccount,
199                &GrpcError::Unavailable,
200                "node is down"
201            )
202            .is_none()
203        );
204    }
205
206    /// A request for a block after the chain tip must parse as `FutureBlock` on every sync
207    /// endpoint.
208    #[test]
209    fn a_block_after_the_chain_tip_parses_as_future_block() {
210        let message = "block_to (5) is greater than chain tip (4)";
211        assert_eq!(
212            parse_node_error(&RpcEndpoint::SyncNotes, &[2], message),
213            Some(EndpointError::NoteSync(NoteSyncError::FutureBlock))
214        );
215        assert_eq!(
216            parse_node_error(&RpcEndpoint::SyncChainMmr, &[2], message),
217            Some(EndpointError::SyncChainMmr(SyncChainMmrError::FutureBlock))
218        );
219        assert_eq!(
220            parse_node_error(&RpcEndpoint::SyncNullifiers, &[4], message),
221            Some(EndpointError::SyncNullifiers(SyncNullifiersError::FutureBlock))
222        );
223        assert_eq!(
224            parse_node_error(&RpcEndpoint::SyncAccountVault, &[4], message),
225            Some(EndpointError::SyncAccountVault(SyncAccountVaultError::FutureBlock))
226        );
227        assert_eq!(
228            parse_node_error(&RpcEndpoint::SyncStorageMaps, &[5], message),
229            Some(EndpointError::SyncStorageMaps(SyncAccountStorageMapsError::FutureBlock))
230        );
231        assert_eq!(
232            parse_node_error(&RpcEndpoint::SyncTransactions, &[5], message),
233            Some(EndpointError::SyncTransactions(SyncTransactionsError::FutureBlock))
234        );
235    }
236}