1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ErrorCode {
13 SessionNotFound,
15 InvalidSessionState,
17 ThresholdNotMet,
19 DuplicateSubmission,
21 SessionExpired,
23 UnauthorizedSigner,
25 SigningFailed,
27 RateLimited,
29 AtCapacity,
31 BackpressureTimeout,
33 PolicyDenied,
35 InvalidShare,
37 NetworkError,
39 ConfigError,
41 StoreError,
43 Internal,
45}
46
47impl ErrorCode {
48 pub fn code(&self) -> u32 {
50 match self {
51 Self::SessionNotFound => 1001,
52 Self::InvalidSessionState => 1002,
53 Self::ThresholdNotMet => 1003,
54 Self::DuplicateSubmission => 1004,
55 Self::SessionExpired => 1005,
56 Self::UnauthorizedSigner => 1006,
57 Self::SigningFailed => 1007,
58 Self::RateLimited => 2001,
59 Self::AtCapacity => 2002,
60 Self::BackpressureTimeout => 2003,
61 Self::PolicyDenied => 3001,
62 Self::InvalidShare => 4001,
63 Self::NetworkError => 5001,
64 Self::ConfigError => 6001,
65 Self::StoreError => 7001,
66 Self::Internal => 9001,
67 }
68 }
69
70 pub fn is_retryable(&self) -> bool {
72 matches!(
73 self,
74 Self::ThresholdNotMet | Self::NetworkError | Self::BackpressureTimeout
75 )
76 }
77
78 pub fn is_client_error(&self) -> bool {
80 matches!(
81 self,
82 Self::SessionNotFound
83 | Self::InvalidSessionState
84 | Self::DuplicateSubmission
85 | Self::UnauthorizedSigner
86 | Self::PolicyDenied
87 | Self::InvalidShare
88 )
89 }
90
91 pub fn description(&self) -> &'static str {
93 match self {
94 Self::SessionNotFound => "The requested session does not exist.",
95 Self::InvalidSessionState => {
96 "The session is not in the required state for this operation."
97 }
98 Self::ThresholdNotMet => "Not enough shares have been submitted to meet the threshold.",
99 Self::DuplicateSubmission => "This signer has already submitted to this session.",
100 Self::SessionExpired => "The session's unlock window has elapsed.",
101 Self::UnauthorizedSigner => "This signer is not authorized for the quorum.",
102 Self::SigningFailed => "The threshold signing engine produced an error.",
103 Self::RateLimited => "Too many requests. Slow down and retry later.",
104 Self::AtCapacity => "The coordinator is at maximum session capacity.",
105 Self::BackpressureTimeout => "The operation timed out due to backpressure.",
106 Self::PolicyDenied => "A policy rule denied the request.",
107 Self::InvalidShare => "The submitted share failed integrity verification.",
108 Self::NetworkError => "A network I/O error occurred.",
109 Self::ConfigError => "Configuration is invalid or incomplete.",
110 Self::StoreError => "A persistence backend error occurred.",
111 Self::Internal => "An unexpected internal error occurred.",
112 }
113 }
114
115 pub fn from_code(code: u32) -> Option<Self> {
117 match code {
118 1001 => Some(Self::SessionNotFound),
119 1002 => Some(Self::InvalidSessionState),
120 1003 => Some(Self::ThresholdNotMet),
121 1004 => Some(Self::DuplicateSubmission),
122 1005 => Some(Self::SessionExpired),
123 1006 => Some(Self::UnauthorizedSigner),
124 1007 => Some(Self::SigningFailed),
125 2001 => Some(Self::RateLimited),
126 2002 => Some(Self::AtCapacity),
127 2003 => Some(Self::BackpressureTimeout),
128 3001 => Some(Self::PolicyDenied),
129 4001 => Some(Self::InvalidShare),
130 5001 => Some(Self::NetworkError),
131 6001 => Some(Self::ConfigError),
132 7001 => Some(Self::StoreError),
133 9001 => Some(Self::Internal),
134 _ => None,
135 }
136 }
137}
138
139impl std::fmt::Display for ErrorCode {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 write!(f, "{} ({})", self.code(), self.description())
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct TypedError {
148 pub code: ErrorCode,
150 pub numeric_code: u32,
152 pub message: String,
154 pub retryable: bool,
156}
157
158impl TypedError {
159 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
161 Self {
162 code,
163 numeric_code: code.code(),
164 message: message.into(),
165 retryable: code.is_retryable(),
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn codes_are_unique() {
176 let codes = [
177 ErrorCode::SessionNotFound,
178 ErrorCode::InvalidSessionState,
179 ErrorCode::ThresholdNotMet,
180 ErrorCode::DuplicateSubmission,
181 ErrorCode::SessionExpired,
182 ErrorCode::UnauthorizedSigner,
183 ErrorCode::SigningFailed,
184 ErrorCode::RateLimited,
185 ErrorCode::AtCapacity,
186 ErrorCode::BackpressureTimeout,
187 ErrorCode::PolicyDenied,
188 ErrorCode::InvalidShare,
189 ErrorCode::NetworkError,
190 ErrorCode::ConfigError,
191 ErrorCode::StoreError,
192 ErrorCode::Internal,
193 ];
194 let mut seen = std::collections::HashSet::new();
195 for code in &codes {
196 assert!(seen.insert(code.code()), "duplicate code: {}", code.code());
197 }
198 }
199
200 #[test]
201 fn from_code_round_trips() {
202 for original in [
203 ErrorCode::SessionNotFound,
204 ErrorCode::ThresholdNotMet,
205 ErrorCode::RateLimited,
206 ErrorCode::Internal,
207 ] {
208 let recovered = ErrorCode::from_code(original.code()).unwrap();
209 assert_eq!(original, recovered);
210 }
211 }
212
213 #[test]
214 fn from_code_unknown_returns_none() {
215 assert!(ErrorCode::from_code(9999).is_none());
216 }
217
218 #[test]
219 fn retryable_codes_correct() {
220 assert!(ErrorCode::ThresholdNotMet.is_retryable());
221 assert!(ErrorCode::NetworkError.is_retryable());
222 assert!(!ErrorCode::SessionNotFound.is_retryable());
223 assert!(!ErrorCode::SigningFailed.is_retryable());
224 }
225
226 #[test]
227 fn client_error_classification() {
228 assert!(ErrorCode::SessionNotFound.is_client_error());
229 assert!(ErrorCode::PolicyDenied.is_client_error());
230 assert!(!ErrorCode::NetworkError.is_client_error());
231 assert!(!ErrorCode::Internal.is_client_error());
232 }
233
234 #[test]
235 fn descriptions_are_non_empty() {
236 for code in [
237 ErrorCode::SessionNotFound,
238 ErrorCode::SigningFailed,
239 ErrorCode::Internal,
240 ] {
241 assert!(!code.description().is_empty());
242 }
243 }
244
245 #[test]
246 fn display_includes_code_and_description() {
247 let s = format!("{}", ErrorCode::ThresholdNotMet);
248 assert!(s.contains("1003"));
249 assert!(s.contains("threshold"));
250 }
251
252 #[test]
253 fn typed_error_carries_metadata() {
254 let err = TypedError::new(ErrorCode::RateLimited, "too many requests");
255 assert_eq!(err.code, ErrorCode::RateLimited);
256 assert_eq!(err.numeric_code, 2001);
257 assert!(!err.retryable); assert_eq!(err.message, "too many requests");
259 }
260
261 #[test]
262 fn typed_error_serializes() {
263 let err = TypedError::new(ErrorCode::Internal, "unexpected");
264 let json = serde_json::to_string(&err).unwrap();
265 assert!(json.contains("internal"));
266 assert!(json.contains("9001"));
267 assert!(json.contains("unexpected"));
268 }
269
270 #[test]
271 fn error_code_serializes_as_snake_case() {
272 let json = serde_json::to_string(&ErrorCode::SessionExpired).unwrap();
273 assert!(json.contains("session_expired"));
274 }
275}