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}