1use thiserror::Error;
22
23use crate::{http::error::DydxHttpError, websocket::error::DydxWsError};
24
25pub type DydxResult<T> = Result<T, DydxError>;
27
28#[derive(Debug, Error)]
30pub enum DydxError {
31 #[error("HTTP error: {0}")]
33 Http(#[from] DydxHttpError),
34
35 #[error("WebSocket error: {0}")]
37 WebSocket(#[from] DydxWsError),
38
39 #[error("gRPC error: {0}")]
41 Grpc(#[from] Box<tonic::Status>),
42
43 #[error("Signing error: {0}")]
45 Signing(String),
46
47 #[error("Encoding error: {0}")]
49 Encoding(#[from] prost::EncodeError),
50
51 #[error("Decoding error: {0}")]
53 Decoding(#[from] prost::DecodeError),
54
55 #[error("JSON error: {message}")]
57 Json {
58 message: String,
59 raw: Option<String>,
61 },
62
63 #[error("Configuration error: {0}")]
65 Config(String),
66
67 #[error("Invalid data: {0}")]
69 InvalidData(String),
70
71 #[error("Invalid order side: {0}")]
73 InvalidOrderSide(String),
74
75 #[error("Unsupported order type: {0}")]
77 UnsupportedOrderType(String),
78
79 #[error("Not implemented: {0}")]
81 NotImplemented(String),
82
83 #[error("Order error: {0}")]
85 Order(String),
86
87 #[error("Parse error: {0}")]
89 Parse(String),
90
91 #[error("Wallet error: {0}")]
93 Wallet(String),
94
95 #[error("Nautilus error: {0}")]
97 Nautilus(#[from] anyhow::Error),
98}
99
100pub const COSMOS_ERROR_CODE_TX_IN_MEMPOOL_CACHE: u32 = 19;
106
107const COSMOS_ERROR_CODE_SEQUENCE_MISMATCH: u32 = 32;
108
109pub const DYDX_ERROR_CODE_CANCEL_ALREADY_IN_MEMCLOB: u32 = 9;
117
118pub const DYDX_ERROR_CODE_ORDER_DOES_NOT_EXIST: u32 = 3006;
123
124const DYDX_ERROR_CODE_ALL_OF_FAILED: u32 = 104;
125
126impl DydxError {
127 #[must_use]
141 pub fn is_sequence_mismatch(&self) -> bool {
142 match self {
143 Self::Grpc(status) => {
144 let msg = status.message();
145 Self::message_indicates_sequence_mismatch(msg)
146 }
147 Self::Nautilus(e) => {
148 let msg = e.to_string();
149 Self::message_indicates_sequence_mismatch(&msg)
150 }
151 _ => false,
152 }
153 }
154
155 fn message_indicates_sequence_mismatch(msg: &str) -> bool {
156 if msg.contains(&format!("code={COSMOS_ERROR_CODE_SEQUENCE_MISMATCH}"))
158 || msg.contains("account sequence mismatch")
159 {
160 return true;
161 }
162 msg.contains(&format!("code={DYDX_ERROR_CODE_ALL_OF_FAILED}")) && msg.contains("sequence")
164 }
165
166 #[must_use]
172 pub fn is_tx_in_mempool(&self) -> bool {
173 match self {
174 Self::Nautilus(e) => {
175 let msg = e.to_string();
176 msg.contains(&format!("code={COSMOS_ERROR_CODE_TX_IN_MEMPOOL_CACHE}"))
177 || msg.contains("tx already in mempool")
178 }
179 _ => false,
180 }
181 }
182
183 #[must_use]
188 pub fn is_cancel_already_in_memclob(&self) -> bool {
189 match self {
190 Self::Nautilus(e) => {
191 let msg = e.to_string();
192 msg.contains(&format!("code={DYDX_ERROR_CODE_CANCEL_ALREADY_IN_MEMCLOB}"))
193 && msg.contains("cancel already exists")
194 }
195 _ => false,
196 }
197 }
198
199 #[must_use]
203 pub fn is_order_does_not_exist(&self) -> bool {
204 match self {
205 Self::Nautilus(e) => {
206 let msg = e.to_string();
207 msg.contains(&format!("code={DYDX_ERROR_CODE_ORDER_DOES_NOT_EXIST}"))
208 || msg.contains("Order Id to cancel does not exist")
209 }
210 _ => false,
211 }
212 }
213
214 #[must_use]
221 pub fn is_benign_cancel_error(&self) -> bool {
222 self.is_tx_in_mempool()
223 || self.is_cancel_already_in_memclob()
224 || self.is_order_does_not_exist()
225 }
226
227 #[must_use]
234 pub fn is_transient(&self) -> bool {
235 if self.is_sequence_mismatch() {
236 return true;
237 }
238
239 match self {
240 Self::Grpc(status) => {
241 matches!(
242 status.code(),
243 tonic::Code::Unavailable
244 | tonic::Code::DeadlineExceeded
245 | tonic::Code::ResourceExhausted
246 )
247 }
248 _ => false,
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use rstest::rstest;
256
257 use super::*;
258
259 #[rstest]
260 fn test_sequence_mismatch_from_code_pattern() {
261 let err = DydxError::Nautilus(anyhow::anyhow!(
263 "Transaction broadcast failed: code=32, log=account sequence mismatch, expected 15, received 14"
264 ));
265 assert!(err.is_sequence_mismatch());
266 }
267
268 #[rstest]
269 fn test_sequence_mismatch_from_text_pattern() {
270 let err = DydxError::Nautilus(anyhow::anyhow!(
271 "account sequence mismatch: expected 100, received 99"
272 ));
273 assert!(err.is_sequence_mismatch());
274 }
275
276 #[rstest]
277 fn test_sequence_mismatch_grpc_error() {
278 let status =
279 tonic::Status::invalid_argument("account sequence mismatch, expected 42, received 41");
280 let err = DydxError::Grpc(Box::new(status));
281 assert!(err.is_sequence_mismatch());
282 }
283
284 #[rstest]
285 fn test_sequence_mismatch_dydx_authenticator_code_104() {
286 let err = DydxError::Nautilus(anyhow::anyhow!(
287 "Transaction broadcast failed: code=104, log=authentication failed for message 0, \
288 authenticator id 966, type AllOf: signature verification failed; \
289 please verify account number (0), sequence (545) and chain-id (dydx-mainnet-1): \
290 Signature verification failed: AllOf verification failed"
291 ));
292 assert!(err.is_sequence_mismatch());
293 }
294
295 #[rstest]
296 fn test_code_104_without_sequence_not_matched() {
297 let err = DydxError::Nautilus(anyhow::anyhow!(
299 "Transaction broadcast failed: code=104, log=authentication failed: invalid pubkey"
300 ));
301 assert!(!err.is_sequence_mismatch());
302 }
303
304 #[rstest]
305 fn test_non_sequence_error_not_matched() {
306 let err = DydxError::Nautilus(anyhow::anyhow!("insufficient funds"));
307 assert!(!err.is_sequence_mismatch());
308 }
309
310 #[rstest]
311 fn test_other_error_variants_not_matched() {
312 let err = DydxError::Config("bad config".to_string());
313 assert!(!err.is_sequence_mismatch());
314
315 let err = DydxError::Order("order rejected".to_string());
316 assert!(!err.is_sequence_mismatch());
317 }
318
319 #[rstest]
320 fn test_is_transient_sequence_mismatch() {
321 let err = DydxError::Nautilus(anyhow::anyhow!("account sequence mismatch"));
322 assert!(err.is_transient());
323 }
324
325 #[rstest]
326 fn test_is_transient_unavailable() {
327 let status = tonic::Status::unavailable("node unavailable");
328 let err = DydxError::Grpc(Box::new(status));
329 assert!(err.is_transient());
330 }
331
332 #[rstest]
333 fn test_is_transient_deadline_exceeded() {
334 let status = tonic::Status::deadline_exceeded("timeout");
335 let err = DydxError::Grpc(Box::new(status));
336 assert!(err.is_transient());
337 }
338
339 #[rstest]
340 fn test_is_not_transient_permission_denied() {
341 let status = tonic::Status::permission_denied("unauthorized");
342 let err = DydxError::Grpc(Box::new(status));
343 assert!(!err.is_transient());
344 }
345
346 #[rstest]
347 fn test_is_not_transient_config_error() {
348 let err = DydxError::Config("invalid".to_string());
349 assert!(!err.is_transient());
350 }
351
352 #[rstest]
353 fn test_benign_cancel_tx_in_mempool() {
354 let err = DydxError::Nautilus(anyhow::anyhow!(
355 "Transaction broadcast failed: code=19, tx already in mempool cache"
356 ));
357 assert!(err.is_tx_in_mempool());
358 assert!(err.is_benign_cancel_error());
359 }
360
361 #[rstest]
362 fn test_benign_cancel_already_in_memclob() {
363 let err = DydxError::Nautilus(anyhow::anyhow!(
364 "Transaction broadcast failed: code=9, cancel already exists in memclob with >= GoodTilBlock"
365 ));
366 assert!(err.is_cancel_already_in_memclob());
367 assert!(err.is_benign_cancel_error());
368 }
369
370 #[rstest]
371 fn test_benign_cancel_order_does_not_exist() {
372 let err = DydxError::Nautilus(anyhow::anyhow!(
373 "Transaction broadcast failed: code=3006, Order Id to cancel does not exist"
374 ));
375 assert!(err.is_order_does_not_exist());
376 assert!(err.is_benign_cancel_error());
377 }
378
379 #[rstest]
380 fn test_non_benign_error_not_treated_as_benign() {
381 let err = DydxError::Nautilus(anyhow::anyhow!("insufficient funds"));
382 assert!(!err.is_benign_cancel_error());
383 }
384
385 #[rstest]
386 fn test_benign_cancel_non_nautilus_variant() {
387 let err = DydxError::Order("order rejected".to_string());
388 assert!(!err.is_benign_cancel_error());
389 }
390}