dig_node_control_interface/
error.rs1use serde::{Deserialize, Serialize};
23
24#[non_exhaustive]
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum ControlErrorCode {
31 ParseError,
33 InvalidRequest,
35 MethodNotFound,
37 InvalidParams,
39 DispatchFailed,
41 Unauthorized,
43 NotSupported,
46 ControlError,
48 WalletNoChainSource,
51 WalletNotSynced,
54 WalletReadFailed,
56 WalletRateLimited,
59}
60
61impl ControlErrorCode {
62 pub const fn code(self) -> i64 {
64 match self {
65 ControlErrorCode::ParseError => -32700,
66 ControlErrorCode::InvalidRequest => -32600,
67 ControlErrorCode::MethodNotFound => -32601,
68 ControlErrorCode::InvalidParams => -32602,
69 ControlErrorCode::DispatchFailed => -32000,
70 ControlErrorCode::Unauthorized => -32030,
71 ControlErrorCode::NotSupported => -32031,
72 ControlErrorCode::ControlError => -32032,
73 ControlErrorCode::WalletNoChainSource => -32040,
74 ControlErrorCode::WalletNotSynced => -32041,
75 ControlErrorCode::WalletReadFailed => -32042,
76 ControlErrorCode::WalletRateLimited => -32043,
77 }
78 }
79
80 pub const fn name(self) -> &'static str {
82 match self {
83 ControlErrorCode::ParseError => "PARSE_ERROR",
84 ControlErrorCode::InvalidRequest => "INVALID_REQUEST",
85 ControlErrorCode::MethodNotFound => "METHOD_NOT_FOUND",
86 ControlErrorCode::InvalidParams => "INVALID_PARAMS",
87 ControlErrorCode::DispatchFailed => "DISPATCH_FAILED",
88 ControlErrorCode::Unauthorized => "UNAUTHORIZED",
89 ControlErrorCode::NotSupported => "NOT_SUPPORTED",
90 ControlErrorCode::ControlError => "CONTROL_ERROR",
91 ControlErrorCode::WalletNoChainSource => "WALLET_NO_CHAIN_SOURCE",
92 ControlErrorCode::WalletNotSynced => "WALLET_NOT_SYNCED",
93 ControlErrorCode::WalletReadFailed => "WALLET_READ_FAILED",
94 ControlErrorCode::WalletRateLimited => "WALLET_RATE_LIMITED",
95 }
96 }
97
98 pub const fn origin(self) -> &'static str {
100 match self {
101 ControlErrorCode::MethodNotFound => "boundary",
102 ControlErrorCode::InvalidParams
104 | ControlErrorCode::WalletNoChainSource
105 | ControlErrorCode::WalletNotSynced
106 | ControlErrorCode::WalletReadFailed
107 | ControlErrorCode::WalletRateLimited => "node",
108 _ => "shell",
109 }
110 }
111
112 pub const fn description(self) -> &'static str {
114 match self {
115 ControlErrorCode::ParseError => "Request body was not valid JSON.",
116 ControlErrorCode::InvalidRequest => {
117 "Request was not a single JSON-RPC object (batch arrays are not supported)."
118 }
119 ControlErrorCode::MethodNotFound => "The control method is not resolved by the node.",
120 ControlErrorCode::InvalidParams => "Invalid or missing method parameters.",
121 ControlErrorCode::DispatchFailed => "The node failed to dispatch the control request.",
122 ControlErrorCode::Unauthorized => {
123 "A control.* method was called without a valid local control token."
124 }
125 ControlErrorCode::NotSupported => {
126 "The requested control operation is not supported on this node build."
127 }
128 ControlErrorCode::ControlError => "A control operation failed at runtime.",
129 ControlErrorCode::WalletNoChainSource => {
130 "A wallet chain read had no live chain source to answer."
131 }
132 ControlErrorCode::WalletNotSynced => {
133 "A wallet chain read of the wallet's own address is still syncing with no fallback."
134 }
135 ControlErrorCode::WalletReadFailed => {
136 "A wallet chain read failed at the DB / chain-source layer."
137 }
138 ControlErrorCode::WalletRateLimited => {
139 "A wallet chain read was refused: the open fallback rate bound is exhausted."
140 }
141 }
142 }
143
144 pub fn from_code(code: i64) -> Option<ControlErrorCode> {
146 ControlErrorCode::ALL
147 .iter()
148 .copied()
149 .find(|c| c.code() == code)
150 }
151
152 pub const ALL: &'static [ControlErrorCode] = &[
154 ControlErrorCode::ParseError,
155 ControlErrorCode::InvalidRequest,
156 ControlErrorCode::MethodNotFound,
157 ControlErrorCode::InvalidParams,
158 ControlErrorCode::DispatchFailed,
159 ControlErrorCode::Unauthorized,
160 ControlErrorCode::NotSupported,
161 ControlErrorCode::ControlError,
162 ControlErrorCode::WalletNoChainSource,
163 ControlErrorCode::WalletNotSynced,
164 ControlErrorCode::WalletReadFailed,
165 ControlErrorCode::WalletRateLimited,
166 ];
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct ControlErrorData {
174 pub code: String,
176 pub origin: String,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ControlError {
187 pub code: i64,
189 pub message: String,
191 pub data: ControlErrorData,
193}
194
195impl ControlError {
196 pub fn of(code: ControlErrorCode, message: impl Into<String>) -> ControlError {
198 ControlError {
199 code: code.code(),
200 message: message.into(),
201 data: ControlErrorData {
202 code: code.name().to_string(),
203 origin: code.origin().to_string(),
204 },
205 }
206 }
207
208 pub fn code_enum(&self) -> Option<ControlErrorCode> {
210 ControlErrorCode::from_code(self.code)
211 }
212}
213
214impl std::fmt::Display for ControlError {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 write!(f, "{} ({}): {}", self.data.code, self.code, self.message)
217 }
218}
219
220impl std::error::Error for ControlError {}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn codes_and_symbols_and_origins_are_stable() {
228 assert_eq!(ControlErrorCode::Unauthorized.code(), -32030);
229 assert_eq!(ControlErrorCode::NotSupported.code(), -32031);
230 assert_eq!(ControlErrorCode::ControlError.code(), -32032);
231 assert_eq!(ControlErrorCode::InvalidParams.name(), "INVALID_PARAMS");
232 assert_eq!(ControlErrorCode::InvalidParams.origin(), "node");
233 assert_eq!(ControlErrorCode::MethodNotFound.origin(), "boundary");
234 assert_eq!(ControlErrorCode::Unauthorized.origin(), "shell");
235 }
236
237 #[test]
238 fn from_code_round_trips_every_code() {
239 for &c in ControlErrorCode::ALL {
240 assert_eq!(ControlErrorCode::from_code(c.code()), Some(c));
241 }
242 assert_eq!(ControlErrorCode::from_code(0), None);
243 }
244
245 #[test]
246 fn every_code_has_a_description() {
247 for &c in ControlErrorCode::ALL {
248 assert!(!c.description().is_empty());
249 }
250 }
251
252 #[test]
253 fn error_serializes_to_the_canonical_json_shape() {
254 let e = ControlError::of(ControlErrorCode::Unauthorized, "no token");
255 let v = serde_json::to_value(&e).unwrap();
256 assert_eq!(v["code"], -32030);
257 assert_eq!(v["message"], "no token");
258 assert_eq!(v["data"]["code"], "UNAUTHORIZED");
259 assert_eq!(v["data"]["origin"], "shell");
260 assert_eq!(e.code_enum(), Some(ControlErrorCode::Unauthorized));
261 }
262
263 #[test]
264 fn display_is_symbol_code_and_message() {
265 let e = ControlError::of(ControlErrorCode::ControlError, "boom");
266 assert_eq!(e.to_string(), "CONTROL_ERROR (-32032): boom");
267 }
268}