dig_node_control_interface/
error.rs1use serde::{Deserialize, Serialize};
25
26#[non_exhaustive]
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum ControlErrorCode {
33 ParseError,
35 InvalidRequest,
37 MethodNotFound,
39 InvalidParams,
41 DispatchFailed,
43 Unauthorized,
45 NotSupported,
48 ControlError,
50 WalletNoChainSource,
53 WalletNotSynced,
56 WalletReadFailed,
58 WalletRateLimited,
61 WalletCoinsReserved,
69 WalletReservationsUnavailable,
76}
77
78impl ControlErrorCode {
79 pub const fn code(self) -> i64 {
81 match self {
82 ControlErrorCode::ParseError => -32700,
83 ControlErrorCode::InvalidRequest => -32600,
84 ControlErrorCode::MethodNotFound => -32601,
85 ControlErrorCode::InvalidParams => -32602,
86 ControlErrorCode::DispatchFailed => -32000,
87 ControlErrorCode::Unauthorized => -32030,
88 ControlErrorCode::NotSupported => -32031,
89 ControlErrorCode::ControlError => -32032,
90 ControlErrorCode::WalletNoChainSource => -32040,
91 ControlErrorCode::WalletNotSynced => -32041,
92 ControlErrorCode::WalletReadFailed => -32042,
93 ControlErrorCode::WalletRateLimited => -32043,
94 ControlErrorCode::WalletCoinsReserved => -32044,
95 ControlErrorCode::WalletReservationsUnavailable => -32045,
96 }
97 }
98
99 pub const fn name(self) -> &'static str {
101 match self {
102 ControlErrorCode::ParseError => "PARSE_ERROR",
103 ControlErrorCode::InvalidRequest => "INVALID_REQUEST",
104 ControlErrorCode::MethodNotFound => "METHOD_NOT_FOUND",
105 ControlErrorCode::InvalidParams => "INVALID_PARAMS",
106 ControlErrorCode::DispatchFailed => "DISPATCH_FAILED",
107 ControlErrorCode::Unauthorized => "UNAUTHORIZED",
108 ControlErrorCode::NotSupported => "NOT_SUPPORTED",
109 ControlErrorCode::ControlError => "CONTROL_ERROR",
110 ControlErrorCode::WalletNoChainSource => "WALLET_NO_CHAIN_SOURCE",
111 ControlErrorCode::WalletNotSynced => "WALLET_NOT_SYNCED",
112 ControlErrorCode::WalletReadFailed => "WALLET_READ_FAILED",
113 ControlErrorCode::WalletRateLimited => "WALLET_RATE_LIMITED",
114 ControlErrorCode::WalletCoinsReserved => "WALLET_COINS_RESERVED",
115 ControlErrorCode::WalletReservationsUnavailable => "WALLET_RESERVATIONS_UNAVAILABLE",
116 }
117 }
118
119 pub const fn origin(self) -> &'static str {
121 match self {
122 ControlErrorCode::MethodNotFound => "boundary",
123 ControlErrorCode::InvalidParams
125 | ControlErrorCode::WalletNoChainSource
126 | ControlErrorCode::WalletNotSynced
127 | ControlErrorCode::WalletReadFailed
128 | ControlErrorCode::WalletRateLimited
129 | ControlErrorCode::WalletCoinsReserved
130 | ControlErrorCode::WalletReservationsUnavailable => "node",
131 _ => "shell",
132 }
133 }
134
135 pub const fn description(self) -> &'static str {
137 match self {
138 ControlErrorCode::ParseError => "Request body was not valid JSON.",
139 ControlErrorCode::InvalidRequest => {
140 "Request was not a single JSON-RPC object (batch arrays are not supported)."
141 }
142 ControlErrorCode::MethodNotFound => "The control method is not resolved by the node.",
143 ControlErrorCode::InvalidParams => "Invalid or missing method parameters.",
144 ControlErrorCode::DispatchFailed => "The node failed to dispatch the control request.",
145 ControlErrorCode::Unauthorized => {
146 "A control.* method was called without a valid local control token."
147 }
148 ControlErrorCode::NotSupported => {
149 "The requested control operation is not supported on this node build."
150 }
151 ControlErrorCode::ControlError => "A control operation failed at runtime.",
152 ControlErrorCode::WalletNoChainSource => {
153 "A wallet chain read had no live chain source to answer."
154 }
155 ControlErrorCode::WalletNotSynced => {
156 "A wallet chain read of the wallet's own address is still syncing with no fallback."
157 }
158 ControlErrorCode::WalletReadFailed => {
159 "A wallet chain read failed at the DB / chain-source layer."
160 }
161 ControlErrorCode::WalletRateLimited => {
162 "A wallet chain read was refused: the open fallback rate bound is exhausted."
163 }
164 ControlErrorCode::WalletCoinsReserved => {
165 "Coins named by this call are committed to a live in-flight spend; nothing was reserved. This is a wait, NOT a shortfall."
166 }
167 ControlErrorCode::WalletReservationsUnavailable => {
168 "The node's coin-reservation set could not be read, so coin selection cannot be trusted."
169 }
170 }
171 }
172
173 pub fn from_code(code: i64) -> Option<ControlErrorCode> {
175 ControlErrorCode::ALL
176 .iter()
177 .copied()
178 .find(|c| c.code() == code)
179 }
180
181 pub const ALL: &'static [ControlErrorCode] = &[
183 ControlErrorCode::ParseError,
184 ControlErrorCode::InvalidRequest,
185 ControlErrorCode::MethodNotFound,
186 ControlErrorCode::InvalidParams,
187 ControlErrorCode::DispatchFailed,
188 ControlErrorCode::Unauthorized,
189 ControlErrorCode::NotSupported,
190 ControlErrorCode::ControlError,
191 ControlErrorCode::WalletNoChainSource,
192 ControlErrorCode::WalletNotSynced,
193 ControlErrorCode::WalletReadFailed,
194 ControlErrorCode::WalletRateLimited,
195 ControlErrorCode::WalletCoinsReserved,
196 ControlErrorCode::WalletReservationsUnavailable,
197 ];
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct ControlErrorData {
205 pub code: String,
207 pub origin: String,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct ControlError {
218 pub code: i64,
220 pub message: String,
222 pub data: ControlErrorData,
224}
225
226impl ControlError {
227 pub fn of(code: ControlErrorCode, message: impl Into<String>) -> ControlError {
229 ControlError {
230 code: code.code(),
231 message: message.into(),
232 data: ControlErrorData {
233 code: code.name().to_string(),
234 origin: code.origin().to_string(),
235 },
236 }
237 }
238
239 pub fn code_enum(&self) -> Option<ControlErrorCode> {
241 ControlErrorCode::from_code(self.code)
242 }
243}
244
245impl std::fmt::Display for ControlError {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 write!(f, "{} ({}): {}", self.data.code, self.code, self.message)
248 }
249}
250
251impl std::error::Error for ControlError {}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn codes_and_symbols_and_origins_are_stable() {
259 assert_eq!(ControlErrorCode::Unauthorized.code(), -32030);
260 assert_eq!(ControlErrorCode::NotSupported.code(), -32031);
261 assert_eq!(ControlErrorCode::ControlError.code(), -32032);
262 assert_eq!(ControlErrorCode::InvalidParams.name(), "INVALID_PARAMS");
263 assert_eq!(ControlErrorCode::InvalidParams.origin(), "node");
264 assert_eq!(ControlErrorCode::MethodNotFound.origin(), "boundary");
265 assert_eq!(ControlErrorCode::Unauthorized.origin(), "shell");
266 }
267
268 #[test]
269 fn from_code_round_trips_every_code() {
270 for &c in ControlErrorCode::ALL {
271 assert_eq!(ControlErrorCode::from_code(c.code()), Some(c));
272 }
273 assert_eq!(ControlErrorCode::from_code(0), None);
274 }
275
276 #[test]
277 fn every_code_has_a_description() {
278 for &c in ControlErrorCode::ALL {
279 assert!(!c.description().is_empty());
280 }
281 }
282
283 #[test]
284 fn error_serializes_to_the_canonical_json_shape() {
285 let e = ControlError::of(ControlErrorCode::Unauthorized, "no token");
286 let v = serde_json::to_value(&e).unwrap();
287 assert_eq!(v["code"], -32030);
288 assert_eq!(v["message"], "no token");
289 assert_eq!(v["data"]["code"], "UNAUTHORIZED");
290 assert_eq!(v["data"]["origin"], "shell");
291 assert_eq!(e.code_enum(), Some(ControlErrorCode::Unauthorized));
292 }
293
294 #[test]
295 fn display_is_symbol_code_and_message() {
296 let e = ControlError::of(ControlErrorCode::ControlError, "boom");
297 assert_eq!(e.to_string(), "CONTROL_ERROR (-32032): boom");
298 }
299}