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 errors that did not reach the venue and can safely
66    /// be retried (transport, timeout, gateway 5xx).
67    #[must_use]
68    pub fn is_retryable(&self) -> bool {
69        match self {
70            Self::Http(e) => should_retry_http_error(e),
71            Self::WebSocket(e) => should_retry_ws_error(e),
72            Self::Auth(_) | Self::Config(_) => false,
73        }
74    }
75
76    /// Returns `true` for errors that indicate a fatal session state
77    /// (deregistered session key, subaccount withdrawn, compliance halt).
78    /// Fatal errors require operator intervention and must not be retried.
79    #[must_use]
80    pub fn is_fatal(&self) -> bool {
81        match self {
82            Self::Http(e) => is_fatal_http_error(e),
83            Self::WebSocket(e) => is_fatal_ws_error(e),
84            Self::Auth(_) => true,
85            Self::Config(_) => true,
86        }
87    }
88}
89
90impl From<serde_json::Error> for DeriveError {
91    fn from(value: serde_json::Error) -> Self {
92        Self::Http(DeriveHttpError::Serde(value))
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use rstest::rstest;
99
100    use super::*;
101
102    #[rstest]
103    fn test_http_transport_is_retryable() {
104        let err: DeriveError = DeriveHttpError::transport("conn reset").into();
105        assert!(err.is_retryable());
106        assert!(!err.is_fatal());
107    }
108
109    #[rstest]
110    fn test_http_jsonrpc_invalid_params_is_not_retryable() {
111        let err: DeriveError = DeriveHttpError::JsonRpc {
112            code: -32602,
113            message: "Invalid params".to_string(),
114            data: None,
115        }
116        .into();
117        assert!(!err.is_retryable());
118    }
119
120    #[rstest]
121    fn test_config_error_is_fatal() {
122        let err = DeriveError::config("missing constants");
123        assert!(!err.is_retryable());
124        assert!(err.is_fatal());
125    }
126
127    #[rstest]
128    fn test_auth_error_is_fatal() {
129        let err: DeriveError = AuthError::ClockBeforeEpoch.into();
130        assert!(!err.is_retryable());
131        assert!(err.is_fatal());
132    }
133
134    #[rstest]
135    fn test_ws_transport_is_retryable() {
136        let err: DeriveError = DeriveWsError::transport("broken pipe").into();
137        assert!(err.is_retryable());
138    }
139}