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}