Skip to main content

px_core/
error.rs

1use std::time::Duration;
2use thiserror::Error;
3
4/// Generates a per-exchange error enum with the five variants every exchange
5/// shares (`Http`, `Api`, `RateLimited`, `AuthRequired`, `MarketNotFound`),
6/// plus any exchange-specific variants passed in the body.
7#[macro_export]
8macro_rules! define_exchange_error {
9    ($ErrorType:ident { $($unique_variant:tt)* }) => {
10        #[derive(Debug, thiserror::Error)]
11        pub enum $ErrorType {
12            #[error("http error: {0}")]
13            Http(#[from] reqwest::Error),
14            #[error("api error: {0}")]
15            Api(String),
16            #[error("rate limited")]
17            RateLimited,
18            #[error("authentication required")]
19            AuthRequired,
20            #[error("market not found: {0}")]
21            MarketNotFound(String),
22            $($unique_variant)*
23        }
24    };
25}
26
27#[derive(Debug, Error)]
28pub enum OpenPxError {
29    #[error("network error: {0}")]
30    Network(#[from] NetworkError),
31
32    #[error("exchange error: {0}")]
33    Exchange(#[from] ExchangeError),
34
35    #[error("websocket error: {0}")]
36    WebSocket(#[from] WebSocketError),
37
38    #[error("signing error: {0}")]
39    Signing(#[from] SigningError),
40
41    #[error("rate limit exceeded")]
42    RateLimitExceeded,
43
44    #[error("serialization error: {0}")]
45    Serialization(#[from] serde_json::Error),
46
47    #[error("configuration error: {0}")]
48    Config(String),
49
50    #[error("invalid input: {0}")]
51    InvalidInput(String),
52
53    #[error("{0}")]
54    Other(String),
55}
56
57impl OpenPxError {
58    /// Whether this error is transient and the operation can be retried.
59    pub fn is_retryable(&self) -> bool {
60        match self {
61            Self::Network(_) => true,
62            Self::RateLimitExceeded => true,
63            Self::Exchange(e) => e.is_retryable(),
64            Self::WebSocket(e) => e.is_retryable(),
65            Self::Signing(_) | Self::Config(_) | Self::InvalidInput(_) => false,
66            Self::Serialization(_) => false,
67            Self::Other(_) => false,
68        }
69    }
70
71    /// Suggested delay before retrying, if applicable.
72    pub fn retry_after(&self) -> Option<Duration> {
73        match self {
74            Self::RateLimitExceeded => Some(Duration::from_secs(1)),
75            Self::Network(NetworkError::Timeout(_)) => Some(Duration::from_millis(500)),
76            Self::Network(_) => Some(Duration::from_millis(100)),
77            Self::WebSocket(e) => e.retry_after(),
78            _ => None,
79        }
80    }
81}
82
83#[derive(Debug, Error)]
84pub enum NetworkError {
85    #[error("http request failed: {0}")]
86    Http(String),
87
88    #[error("timeout after {0}ms")]
89    Timeout(u64),
90
91    #[error("connection failed: {0}")]
92    Connection(String),
93}
94
95#[derive(Debug, Error)]
96pub enum ExchangeError {
97    #[error("market not found: {0}")]
98    MarketNotFound(String),
99
100    #[error("invalid order: {0}")]
101    InvalidOrder(String),
102
103    #[error("order rejected: {0}")]
104    OrderRejected(String),
105
106    #[error("insufficient funds: {0}")]
107    InsufficientFunds(String),
108
109    #[error("authentication failed: {0}")]
110    Authentication(String),
111
112    #[error("not supported: {0}")]
113    NotSupported(String),
114
115    #[error("api error: {0}")]
116    Api(String),
117}
118
119#[derive(Debug, Clone, Error)]
120pub enum WebSocketError {
121    #[error("connection error: {0}")]
122    Connection(String),
123
124    #[error("connection closed")]
125    Closed,
126
127    #[error("protocol error: {0}")]
128    Protocol(String),
129
130    #[error("subscription failed: {0}")]
131    Subscription(String),
132}
133
134impl WebSocketError {
135    pub fn is_retryable(&self) -> bool {
136        matches!(self, Self::Connection(_) | Self::Closed)
137    }
138
139    pub fn retry_after(&self) -> Option<Duration> {
140        match self {
141            Self::Connection(_) | Self::Closed => Some(Duration::from_millis(500)),
142            _ => None,
143        }
144    }
145}
146
147impl ExchangeError {
148    pub fn is_retryable(&self) -> bool {
149        matches!(self, Self::Api(_))
150    }
151}
152
153#[derive(Debug, Error)]
154pub enum SigningError {
155    #[error("invalid private key")]
156    InvalidKey,
157
158    #[error("signing failed: {0}")]
159    SigningFailed(String),
160
161    #[error("unsupported operation: {0}")]
162    Unsupported(String),
163}