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
100const COSMOS_ERROR_CODE_SEQUENCE_MISMATCH: u32 = 32;
103
104const DYDX_ERROR_CODE_ALL_OF_FAILED: u32 = 104;
109
110impl DydxError {
111 #[must_use]
125 pub fn is_sequence_mismatch(&self) -> bool {
126 match self {
127 Self::Grpc(status) => {
128 let msg = status.message();
129 Self::message_indicates_sequence_mismatch(msg)
130 }
131 Self::Nautilus(e) => {
132 let msg = e.to_string();
133 Self::message_indicates_sequence_mismatch(&msg)
134 }
135 _ => false,
136 }
137 }
138
139 fn message_indicates_sequence_mismatch(msg: &str) -> bool {
146 if msg.contains(&format!("code={COSMOS_ERROR_CODE_SEQUENCE_MISMATCH}"))
148 || msg.contains("account sequence mismatch")
149 {
150 return true;
151 }
152 msg.contains(&format!("code={DYDX_ERROR_CODE_ALL_OF_FAILED}")) && msg.contains("sequence")
154 }
155
156 #[must_use]
163 pub fn is_transient(&self) -> bool {
164 if self.is_sequence_mismatch() {
165 return true;
166 }
167
168 match self {
169 Self::Grpc(status) => {
170 matches!(
171 status.code(),
172 tonic::Code::Unavailable
173 | tonic::Code::DeadlineExceeded
174 | tonic::Code::ResourceExhausted
175 )
176 }
177 _ => false,
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use rstest::rstest;
185
186 use super::*;
187
188 #[rstest]
189 fn test_sequence_mismatch_from_code_pattern() {
190 let err = DydxError::Nautilus(anyhow::anyhow!(
192 "Transaction broadcast failed: code=32, log=account sequence mismatch, expected 15, received 14"
193 ));
194 assert!(err.is_sequence_mismatch());
195 }
196
197 #[rstest]
198 fn test_sequence_mismatch_from_text_pattern() {
199 let err = DydxError::Nautilus(anyhow::anyhow!(
200 "account sequence mismatch: expected 100, received 99"
201 ));
202 assert!(err.is_sequence_mismatch());
203 }
204
205 #[rstest]
206 fn test_sequence_mismatch_grpc_error() {
207 let status =
208 tonic::Status::invalid_argument("account sequence mismatch, expected 42, received 41");
209 let err = DydxError::Grpc(Box::new(status));
210 assert!(err.is_sequence_mismatch());
211 }
212
213 #[rstest]
214 fn test_sequence_mismatch_dydx_authenticator_code_104() {
215 let err = DydxError::Nautilus(anyhow::anyhow!(
216 "Transaction broadcast failed: code=104, log=authentication failed for message 0, \
217 authenticator id 966, type AllOf: signature verification failed; \
218 please verify account number (0), sequence (545) and chain-id (dydx-mainnet-1): \
219 Signature verification failed: AllOf verification failed"
220 ));
221 assert!(err.is_sequence_mismatch());
222 }
223
224 #[rstest]
225 fn test_code_104_without_sequence_not_matched() {
226 let err = DydxError::Nautilus(anyhow::anyhow!(
228 "Transaction broadcast failed: code=104, log=authentication failed: invalid pubkey"
229 ));
230 assert!(!err.is_sequence_mismatch());
231 }
232
233 #[rstest]
234 fn test_non_sequence_error_not_matched() {
235 let err = DydxError::Nautilus(anyhow::anyhow!("insufficient funds"));
236 assert!(!err.is_sequence_mismatch());
237 }
238
239 #[rstest]
240 fn test_other_error_variants_not_matched() {
241 let err = DydxError::Config("bad config".to_string());
242 assert!(!err.is_sequence_mismatch());
243
244 let err = DydxError::Order("order rejected".to_string());
245 assert!(!err.is_sequence_mismatch());
246 }
247
248 #[rstest]
249 fn test_is_transient_sequence_mismatch() {
250 let err = DydxError::Nautilus(anyhow::anyhow!("account sequence mismatch"));
251 assert!(err.is_transient());
252 }
253
254 #[rstest]
255 fn test_is_transient_unavailable() {
256 let status = tonic::Status::unavailable("node unavailable");
257 let err = DydxError::Grpc(Box::new(status));
258 assert!(err.is_transient());
259 }
260
261 #[rstest]
262 fn test_is_transient_deadline_exceeded() {
263 let status = tonic::Status::deadline_exceeded("timeout");
264 let err = DydxError::Grpc(Box::new(status));
265 assert!(err.is_transient());
266 }
267
268 #[rstest]
269 fn test_is_not_transient_permission_denied() {
270 let status = tonic::Status::permission_denied("unauthorized");
271 let err = DydxError::Grpc(Box::new(status));
272 assert!(!err.is_transient());
273 }
274
275 #[rstest]
276 fn test_is_not_transient_config_error() {
277 let err = DydxError::Config("invalid".to_string());
278 assert!(!err.is_transient());
279 }
280}