Skip to main content

polymarket_client_sdk/
error.rs

1use std::backtrace::Backtrace;
2use std::error::Error as StdError;
3use std::fmt;
4
5use alloy::primitives::ChainId;
6use alloy::primitives::ruint::ParseError;
7use hmac::digest::InvalidLength;
8/// HTTP method type, re-exported for use with error inspection.
9pub use reqwest::Method;
10/// HTTP status code type, re-exported for use with error inspection.
11pub use reqwest::StatusCode;
12use reqwest::header;
13
14#[non_exhaustive]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Kind {
17    /// Error related to non-successful HTTP call
18    Status,
19    /// Error related to invalid state within polymarket-client-sdk
20    Validation,
21    /// Error related to synchronization of authenticated clients logging in and out
22    Synchronization,
23    /// Internal error from dependencies
24    Internal,
25    /// Error related to WebSocket connections
26    WebSocket,
27    /// Error related to geographic restrictions blocking access
28    Geoblock,
29}
30
31#[derive(Debug)]
32pub struct Error {
33    kind: Kind,
34    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
35    backtrace: Backtrace,
36}
37
38impl Error {
39    pub fn with_source<S: StdError + Send + Sync + 'static>(kind: Kind, source: S) -> Self {
40        Self {
41            kind,
42            source: Some(Box::new(source)),
43            backtrace: Backtrace::capture(),
44        }
45    }
46
47    pub fn kind(&self) -> Kind {
48        self.kind
49    }
50
51    pub fn backtrace(&self) -> &Backtrace {
52        &self.backtrace
53    }
54
55    pub fn inner(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
56        self.source.as_deref()
57    }
58
59    pub fn downcast_ref<E: StdError + 'static>(&self) -> Option<&E> {
60        let e = self.source.as_deref()?;
61        e.downcast_ref::<E>()
62    }
63
64    pub fn validation<S: Into<String>>(message: S) -> Self {
65        Validation {
66            reason: message.into(),
67        }
68        .into()
69    }
70
71    pub fn status<S: Into<String>>(
72        status_code: StatusCode,
73        method: Method,
74        path: String,
75        message: S,
76    ) -> Self {
77        Status {
78            status_code,
79            method,
80            path,
81            message: message.into(),
82        }
83        .into()
84    }
85
86    #[must_use]
87    pub fn missing_contract_config(chain_id: ChainId, neg_risk: bool) -> Self {
88        MissingContractConfig { chain_id, neg_risk }.into()
89    }
90}
91
92impl fmt::Display for Error {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match &self.source {
95            Some(src) => write!(f, "{:?}: {}", self.kind, src),
96            None => write!(f, "{:?}", self.kind),
97        }
98    }
99}
100
101impl StdError for Error {
102    fn source(&self) -> Option<&(dyn StdError + 'static)> {
103        self.source
104            .as_deref()
105            .map(|e| e as &(dyn StdError + 'static))
106    }
107}
108
109#[non_exhaustive]
110#[derive(Debug)]
111pub struct Status {
112    pub status_code: StatusCode,
113    pub method: Method,
114    pub path: String,
115    pub message: String,
116}
117
118impl fmt::Display for Status {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(
121            f,
122            "error({}) making {} call to {} with {}",
123            self.status_code, self.method, self.path, self.message
124        )
125    }
126}
127
128impl StdError for Status {}
129
130#[non_exhaustive]
131#[derive(Debug)]
132pub struct Validation {
133    pub reason: String,
134}
135
136impl fmt::Display for Validation {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "invalid: {}", self.reason)
139    }
140}
141
142impl StdError for Validation {}
143
144#[non_exhaustive]
145#[derive(Debug)]
146pub struct Synchronization;
147
148impl fmt::Display for Synchronization {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(
151            f,
152            "synchronization error: multiple threads are attempting to log in or log out"
153        )
154    }
155}
156
157impl StdError for Synchronization {}
158
159#[non_exhaustive]
160#[derive(Debug, Clone, Copy)]
161pub struct MissingContractConfig {
162    pub chain_id: ChainId,
163    pub neg_risk: bool,
164}
165
166impl fmt::Display for MissingContractConfig {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(
169            f,
170            "missing contract config for chain id {} with neg_risk = {}",
171            self.chain_id, self.neg_risk,
172        )
173    }
174}
175
176impl std::error::Error for MissingContractConfig {}
177
178impl From<MissingContractConfig> for Error {
179    fn from(err: MissingContractConfig) -> Self {
180        Error::with_source(Kind::Internal, err)
181    }
182}
183
184/// Error indicating that the user is blocked from accessing Polymarket due to geographic
185/// restrictions.
186///
187/// This error contains information about the user's detected location.
188#[non_exhaustive]
189#[derive(Debug, Clone)]
190pub struct Geoblock {
191    /// The detected IP address
192    pub ip: String,
193    /// ISO 3166-1 alpha-2 country code
194    pub country: String,
195    /// Region/state code
196    pub region: String,
197}
198
199impl fmt::Display for Geoblock {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(
202            f,
203            "access blocked from country: {}, region: {}, ip: {}",
204            self.country, self.region, self.ip
205        )
206    }
207}
208
209impl StdError for Geoblock {}
210
211impl From<Geoblock> for Error {
212    fn from(err: Geoblock) -> Self {
213        Error::with_source(Kind::Geoblock, err)
214    }
215}
216
217impl From<base64::DecodeError> for Error {
218    fn from(e: base64::DecodeError) -> Self {
219        Error::with_source(Kind::Internal, e)
220    }
221}
222
223impl From<reqwest::Error> for Error {
224    fn from(e: reqwest::Error) -> Self {
225        Error::with_source(Kind::Internal, e)
226    }
227}
228
229impl From<header::InvalidHeaderValue> for Error {
230    fn from(e: header::InvalidHeaderValue) -> Self {
231        Error::with_source(Kind::Internal, e)
232    }
233}
234
235impl From<InvalidLength> for Error {
236    fn from(e: InvalidLength) -> Self {
237        Error::with_source(Kind::Internal, e)
238    }
239}
240
241impl From<serde_json::Error> for Error {
242    fn from(e: serde_json::Error) -> Self {
243        Error::with_source(Kind::Internal, e)
244    }
245}
246
247impl From<alloy::signers::Error> for Error {
248    fn from(e: alloy::signers::Error) -> Self {
249        Error::with_source(Kind::Internal, e)
250    }
251}
252
253impl From<url::ParseError> for Error {
254    fn from(e: url::ParseError) -> Self {
255        Error::with_source(Kind::Internal, e)
256    }
257}
258
259impl From<ParseError> for Error {
260    fn from(e: ParseError) -> Self {
261        Error::with_source(Kind::Internal, e)
262    }
263}
264
265impl From<Validation> for Error {
266    fn from(err: Validation) -> Self {
267        Error::with_source(Kind::Validation, err)
268    }
269}
270
271impl From<Status> for Error {
272    fn from(err: Status) -> Self {
273        Error::with_source(Kind::Status, err)
274    }
275}
276
277impl From<Synchronization> for Error {
278    fn from(err: Synchronization) -> Self {
279        Error::with_source(Kind::Synchronization, err)
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn geoblock_display_should_succeed() {
289        let geoblock = Geoblock {
290            ip: "192.168.1.1".to_owned(),
291            country: "US".to_owned(),
292            region: "NY".to_owned(),
293        };
294
295        assert_eq!(
296            geoblock.to_string(),
297            "access blocked from country: US, region: NY, ip: 192.168.1.1"
298        );
299    }
300
301    #[test]
302    fn geoblock_into_error_should_succeed() {
303        let geoblock = Geoblock {
304            ip: "10.0.0.1".to_owned(),
305            country: "CU".to_owned(),
306            region: "HAV".to_owned(),
307        };
308
309        let error: Error = geoblock.into();
310
311        assert_eq!(error.kind(), Kind::Geoblock);
312        assert!(error.to_string().contains("CU"));
313    }
314}