1use thiserror::Error;
22
23use crate::{
24 http::error::DydxHttpError,
25 proto::cosmos_sdk_proto::prost::{DecodeError, EncodeError},
26 websocket::error::DydxWsError,
27};
28
29pub type DydxResult<T> = Result<T, DydxError>;
31
32#[derive(Debug, Error)]
34pub enum DydxError {
35 #[error("HTTP error: {0}")]
37 Http(#[from] DydxHttpError),
38
39 #[error("WebSocket error: {0}")]
41 WebSocket(#[from] DydxWsError),
42
43 #[error("gRPC error: {0}")]
45 Grpc(#[from] Box<tonic::Status>),
46
47 #[error("Signing error: {0}")]
49 Signing(String),
50
51 #[error("Encoding error: {0}")]
53 Encoding(#[from] EncodeError),
54
55 #[error("Decoding error: {0}")]
57 Decoding(#[from] DecodeError),
58
59 #[error("JSON error: {message}")]
61 Json {
62 message: String,
63 raw: Option<String>,
65 },
66
67 #[error("Configuration error: {0}")]
69 Config(String),
70
71 #[error("Invalid data: {0}")]
73 InvalidData(String),
74
75 #[error("Invalid order side: {0}")]
77 InvalidOrderSide(String),
78
79 #[error("Unsupported order type: {0}")]
81 UnsupportedOrderType(String),
82
83 #[error("Not implemented: {0}")]
85 NotImplemented(String),
86
87 #[error("Order error: {0}")]
89 Order(String),
90
91 #[error("Parse error: {0}")]
93 Parse(String),
94
95 #[error("Wallet error: {0}")]
97 Wallet(String),
98
99 #[error("Nautilus error: {0}")]
101 Nautilus(#[from] anyhow::Error),
102}
103
104pub const COSMOS_ERROR_CODE_TX_IN_MEMPOOL_CACHE: u32 = 19;
110
111const COSMOS_ERROR_CODE_SEQUENCE_MISMATCH: u32 = 32;
112
113pub const DYDX_ERROR_CODE_CANCEL_ALREADY_IN_MEMCLOB: u32 = 9;
121
122pub const DYDX_ERROR_CODE_ORDER_DOES_NOT_EXIST: u32 = 3006;
127
128const DYDX_ERROR_CODE_ALL_OF_FAILED: u32 = 104;
129
130impl DydxError {
131 #[must_use]
145 pub fn is_sequence_mismatch(&self) -> bool {
146 match self {
147 Self::Grpc(status) => {
148 let msg = status.message();
149 Self::message_indicates_sequence_mismatch(msg)
150 }
151 Self::Nautilus(e) => {
152 let msg = e.to_string();
153 Self::message_indicates_sequence_mismatch(&msg)
154 }
155 _ => false,
156 }
157 }
158
159 fn message_indicates_sequence_mismatch(msg: &str) -> bool {
160 if msg.contains(&format!("code={COSMOS_ERROR_CODE_SEQUENCE_MISMATCH}"))
162 || msg.contains("account sequence mismatch")
163 {
164 return true;
165 }
166 msg.contains(&format!("code={DYDX_ERROR_CODE_ALL_OF_FAILED}")) && msg.contains("sequence")
168 }
169
170 #[must_use]
176 pub fn is_tx_in_mempool(&self) -> bool {
177 match self {
178 Self::Nautilus(e) => {
179 let msg = e.to_string();
180 msg.contains(&format!("code={COSMOS_ERROR_CODE_TX_IN_MEMPOOL_CACHE}"))
181 || msg.contains("tx already in mempool")
182 }
183 _ => false,
184 }
185 }
186
187 #[must_use]
192 pub fn is_cancel_already_in_memclob(&self) -> bool {
193 match self {
194 Self::Nautilus(e) => {
195 let msg = e.to_string();
196 msg.contains(&format!("code={DYDX_ERROR_CODE_CANCEL_ALREADY_IN_MEMCLOB}"))
197 && msg.contains("cancel already exists")
198 }
199 _ => false,
200 }
201 }
202
203 #[must_use]
207 pub fn is_order_does_not_exist(&self) -> bool {
208 match self {
209 Self::Nautilus(e) => {
210 let msg = e.to_string();
211 msg.contains(&format!("code={DYDX_ERROR_CODE_ORDER_DOES_NOT_EXIST}"))
212 || msg.contains("Order Id to cancel does not exist")
213 }
214 _ => false,
215 }
216 }
217
218 #[must_use]
225 pub fn is_benign_cancel_error(&self) -> bool {
226 self.is_tx_in_mempool()
227 || self.is_cancel_already_in_memclob()
228 || self.is_order_does_not_exist()
229 }
230
231 #[must_use]
238 pub fn is_transient(&self) -> bool {
239 if self.is_sequence_mismatch() {
240 return true;
241 }
242
243 match self {
244 Self::Grpc(status) => {
245 matches!(
246 status.code(),
247 tonic::Code::Unavailable
248 | tonic::Code::DeadlineExceeded
249 | tonic::Code::ResourceExhausted
250 )
251 }
252 _ => false,
253 }
254 }
255
256 #[must_use]
266 pub fn is_definitive_broadcast_rejection(&self) -> bool {
267 if self.is_benign_cancel_error() || self.is_transient() {
268 return false;
269 }
270
271 match self {
272 Self::Nautilus(e) => e
273 .to_string()
274 .contains("Transaction broadcast failed: code="),
275 _ => false,
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use rstest::rstest;
283
284 use super::*;
285
286 #[rstest]
287 fn test_sequence_mismatch_from_code_pattern() {
288 let err = DydxError::Nautilus(anyhow::anyhow!(
290 "Transaction broadcast failed: code=32, log=account sequence mismatch, expected 15, received 14"
291 ));
292 assert!(err.is_sequence_mismatch());
293 }
294
295 #[rstest]
296 fn test_sequence_mismatch_from_text_pattern() {
297 let err = DydxError::Nautilus(anyhow::anyhow!(
298 "account sequence mismatch: expected 100, received 99"
299 ));
300 assert!(err.is_sequence_mismatch());
301 }
302
303 #[rstest]
304 fn test_sequence_mismatch_grpc_error() {
305 let status =
306 tonic::Status::invalid_argument("account sequence mismatch, expected 42, received 41");
307 let err = DydxError::Grpc(Box::new(status));
308 assert!(err.is_sequence_mismatch());
309 }
310
311 #[rstest]
312 fn test_sequence_mismatch_dydx_authenticator_code_104() {
313 let err = DydxError::Nautilus(anyhow::anyhow!(
314 "Transaction broadcast failed: code=104, log=authentication failed for message 0, \
315 authenticator id 966, type AllOf: signature verification failed; \
316 please verify account number (0), sequence (545) and chain-id (dydx-mainnet-1): \
317 Signature verification failed: AllOf verification failed"
318 ));
319 assert!(err.is_sequence_mismatch());
320 }
321
322 #[rstest]
323 fn test_code_104_without_sequence_not_matched() {
324 let err = DydxError::Nautilus(anyhow::anyhow!(
326 "Transaction broadcast failed: code=104, log=authentication failed: invalid pubkey"
327 ));
328 assert!(!err.is_sequence_mismatch());
329 }
330
331 #[rstest]
332 fn test_non_sequence_error_not_matched() {
333 let err = DydxError::Nautilus(anyhow::anyhow!("insufficient funds"));
334 assert!(!err.is_sequence_mismatch());
335 }
336
337 #[rstest]
338 fn test_other_error_variants_not_matched() {
339 let err = DydxError::Config("bad config".to_string());
340 assert!(!err.is_sequence_mismatch());
341
342 let err = DydxError::Order("order rejected".to_string());
343 assert!(!err.is_sequence_mismatch());
344 }
345
346 #[rstest]
347 fn test_is_transient_sequence_mismatch() {
348 let err = DydxError::Nautilus(anyhow::anyhow!("account sequence mismatch"));
349 assert!(err.is_transient());
350 }
351
352 #[rstest]
353 fn test_is_transient_unavailable() {
354 let status = tonic::Status::unavailable("node unavailable");
355 let err = DydxError::Grpc(Box::new(status));
356 assert!(err.is_transient());
357 }
358
359 #[rstest]
360 fn test_is_transient_deadline_exceeded() {
361 let status = tonic::Status::deadline_exceeded("timeout");
362 let err = DydxError::Grpc(Box::new(status));
363 assert!(err.is_transient());
364 }
365
366 #[rstest]
367 fn test_is_not_transient_permission_denied() {
368 let status = tonic::Status::permission_denied("unauthorized");
369 let err = DydxError::Grpc(Box::new(status));
370 assert!(!err.is_transient());
371 }
372
373 #[rstest]
374 fn test_is_not_transient_config_error() {
375 let err = DydxError::Config("invalid".to_string());
376 assert!(!err.is_transient());
377 }
378
379 #[rstest]
380 fn test_benign_cancel_tx_in_mempool() {
381 let err = DydxError::Nautilus(anyhow::anyhow!(
382 "Transaction broadcast failed: code=19, tx already in mempool cache"
383 ));
384 assert!(err.is_tx_in_mempool());
385 assert!(err.is_benign_cancel_error());
386 }
387
388 #[rstest]
389 fn test_benign_cancel_already_in_memclob() {
390 let err = DydxError::Nautilus(anyhow::anyhow!(
391 "Transaction broadcast failed: code=9, cancel already exists in memclob with >= GoodTilBlock"
392 ));
393 assert!(err.is_cancel_already_in_memclob());
394 assert!(err.is_benign_cancel_error());
395 }
396
397 #[rstest]
398 fn test_benign_cancel_order_does_not_exist() {
399 let err = DydxError::Nautilus(anyhow::anyhow!(
400 "Transaction broadcast failed: code=3006, Order Id to cancel does not exist"
401 ));
402 assert!(err.is_order_does_not_exist());
403 assert!(err.is_benign_cancel_error());
404 }
405
406 #[rstest]
407 fn test_non_benign_error_not_treated_as_benign() {
408 let err = DydxError::Nautilus(anyhow::anyhow!("insufficient funds"));
409 assert!(!err.is_benign_cancel_error());
410 }
411
412 #[rstest]
413 fn test_benign_cancel_non_nautilus_variant() {
414 let err = DydxError::Order("order rejected".to_string());
415 assert!(!err.is_benign_cancel_error());
416 }
417
418 #[rstest]
419 fn test_definitive_broadcast_rejection_checktx_code() {
420 let err = DydxError::Nautilus(anyhow::anyhow!(
421 "Transaction broadcast failed: code=2000, log=insufficient margin"
422 ));
423 assert!(err.is_definitive_broadcast_rejection());
424 }
425
426 #[rstest]
427 fn test_definitive_broadcast_rejection_excludes_benign_codes() {
428 let err = DydxError::Nautilus(anyhow::anyhow!(
429 "Transaction broadcast failed: code=19, tx already in mempool cache"
430 ));
431 assert!(!err.is_definitive_broadcast_rejection());
432
433 let err = DydxError::Nautilus(anyhow::anyhow!(
434 "Transaction broadcast failed: code=3006, Order Id to cancel does not exist"
435 ));
436 assert!(!err.is_definitive_broadcast_rejection());
437 }
438
439 #[rstest]
440 fn test_definitive_broadcast_rejection_excludes_transport_errors() {
441 let status = tonic::Status::unavailable("node unavailable");
442 let err = DydxError::Grpc(Box::new(status));
443 assert!(!err.is_definitive_broadcast_rejection());
444
445 let err = DydxError::Nautilus(anyhow::anyhow!("connection reset by peer"));
446 assert!(!err.is_definitive_broadcast_rejection());
447 }
448
449 #[rstest]
450 fn test_definitive_broadcast_rejection_excludes_sequence_mismatch() {
451 let err = DydxError::Nautilus(anyhow::anyhow!(
452 "Transaction broadcast failed: code=32, log=account sequence mismatch, expected 15, received 14"
453 ));
454 assert!(!err.is_definitive_broadcast_rejection());
455
456 let err = DydxError::Nautilus(anyhow::anyhow!(
457 "Transaction broadcast failed: code=104, log=signature verification failed; please verify sequence (545)"
458 ));
459 assert!(!err.is_definitive_broadcast_rejection());
460 }
461}