Skip to main content

nautilus_derive/common/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Adapter-level error aggregation for the Derive integration.
17//!
18//! Component clients raise their own error taxonomies ([`DeriveHttpError`],
19//! [`DeriveWsError`], [`AuthError`]); [`DeriveError`] unifies them at the
20//! adapter boundary so callers can match on a single type without losing the
21//! per-component detail.
22
23use thiserror::Error;
24
25use crate::{
26    common::retry::{
27        is_fatal_http_error, is_fatal_ws_error, should_retry_http_error, should_retry_ws_error,
28    },
29    http::DeriveHttpError,
30    signing::auth::AuthError,
31    websocket::DeriveWsError,
32};
33
34/// Result alias for adapter-level operations.
35pub type Result<T> = std::result::Result<T, DeriveError>;
36
37/// Unified error type aggregating the Derive adapter's component errors.
38#[derive(Debug, Error)]
39pub enum DeriveError {
40    /// HTTP transport, JSON-RPC, or credential errors raised by [`DeriveHttpError`].
41    #[error("HTTP error: {0}")]
42    Http(#[from] DeriveHttpError),
43
44    /// WebSocket transport, framing, or login errors raised by [`DeriveWsError`].
45    #[error("WebSocket error: {0}")]
46    WebSocket(#[from] DeriveWsError),
47
48    /// Signing or session authentication errors.
49    #[error("auth error: {0}")]
50    Auth(#[from] AuthError),
51
52    /// Configuration error surfaced during client construction (placeholder
53    /// constants, invalid hex, missing credentials).
54    #[error("configuration error: {0}")]
55    Config(String),
56}
57
58impl DeriveError {
59    /// Constructs a [`DeriveError::Config`] error.
60    #[must_use]
61    pub fn config(msg: impl Into<String>) -> Self {
62        Self::Config(msg.into())
63    }
64
65    /// Returns `true` for transient errors covered by the retry classifiers.
66    ///
67    /// This does not prove that a request was unsent. State-changing operations
68    /// require outcome reconciliation before deciding whether to send again.
69    #[must_use]
70    pub fn is_retryable(&self) -> bool {
71        match self {
72            Self::Http(e) => should_retry_http_error(e),
73            Self::WebSocket(e) => should_retry_ws_error(e),
74            Self::Auth(_) | Self::Config(_) => false,
75        }
76    }
77
78    /// Returns `true` for errors that indicate a fatal session state
79    /// (deregistered session key, subaccount withdrawn, compliance halt).
80    /// Fatal errors require operator intervention and must not be retried.
81    #[must_use]
82    pub fn is_fatal(&self) -> bool {
83        match self {
84            Self::Http(e) => is_fatal_http_error(e),
85            Self::WebSocket(e) => is_fatal_ws_error(e),
86            Self::Auth(_) => true,
87            Self::Config(_) => true,
88        }
89    }
90}
91
92impl From<serde_json::Error> for DeriveError {
93    fn from(value: serde_json::Error) -> Self {
94        Self::Http(DeriveHttpError::Serde(value))
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use rstest::rstest;
101
102    use super::*;
103
104    #[rstest]
105    fn test_http_transport_is_retryable() {
106        let err: DeriveError = DeriveHttpError::transport("conn reset").into();
107        assert!(err.is_retryable());
108        assert!(!err.is_fatal());
109    }
110
111    #[rstest]
112    fn test_http_jsonrpc_invalid_params_is_not_retryable() {
113        let err: DeriveError = DeriveHttpError::JsonRpc {
114            code: -32602,
115            message: "Invalid params".to_string(),
116            data: None,
117            retry_after: None,
118        }
119        .into();
120
121        assert!(!err.is_retryable());
122    }
123
124    #[rstest]
125    fn test_config_error_is_fatal() {
126        let err = DeriveError::config("missing constants");
127        assert!(!err.is_retryable());
128        assert!(err.is_fatal());
129    }
130
131    #[rstest]
132    fn test_auth_error_is_fatal() {
133        let err: DeriveError = AuthError::ClockBeforeEpoch.into();
134        assert!(!err.is_retryable());
135        assert!(err.is_fatal());
136    }
137
138    #[rstest]
139    fn test_ws_transport_is_retryable() {
140        let err: DeriveError = DeriveWsError::transport("broken pipe").into();
141        assert!(err.is_retryable());
142    }
143}