Skip to main content

degenbot_core/
errors.rs

1//! Error types for degenbot Rust extensions.
2
3/// Raw `PyErr` type for the gated conversion impls.
4///
5/// Only available when the `pyo3` feature is enabled. The conversions live here
6/// (with the error types) rather than in a downstream crate because the orphan
7/// rule forbids `impl ForeignTrait for ForeignType` elsewhere.
8#[cfg(feature = "pyo3")]
9use pyo3::{exceptions::PyValueError, PyErr};
10
11/// Errors that can occur during tick math calculations.
12#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum TickMathError {
15    /// Invalid tick value outside the valid range [-887272, 887272].
16    #[error("Invalid tick value: {0}. Must be in range [-887272, 887272]")]
17    InvalidTick(i32),
18    /// Sqrt ratio value outside the valid [`MIN_SQRT_RATIO`, `MAX_SQRT_RATIO`) range.
19    #[error("Sqrt ratio {actual} out of bounds [{min}, {max})")]
20    SqrtRatioOutOfBounds {
21        /// The value that was out of bounds.
22        actual: alloy::primitives::aliases::U160,
23        /// Minimum valid sqrt ratio.
24        min: alloy::primitives::aliases::U160,
25        /// Maximum valid sqrt ratio (exclusive).
26        max: alloy::primitives::aliases::U160,
27    },
28}
29
30/// Errors from the concentrated-liquidity math library.
31#[derive(Debug, thiserror::Error, PartialEq)]
32#[non_exhaustive]
33pub enum ClMathError {
34    /// Division by zero.
35    #[error("DIVISION BY ZERO")]
36    DivisionByZero,
37    /// Result would overflow uint256.
38    #[error("product > MAX_UINT256")]
39    Uint256Overflow,
40    /// Zero input where a non-zero value is required.
41    #[error("Zero input")]
42    ZeroInput,
43    /// Invalid price (zero where non-zero is required).
44    #[error("InvalidPrice")]
45    InvalidPrice,
46    /// Invalid price or liquidity (either is zero where non-zero is required).
47    #[error("InvalidPriceOrLiquidity")]
48    InvalidPriceOrLiquidity,
49    /// Price overflow (exceeds uint160 or subtraction underflow).
50    #[error("PriceOverflow")]
51    PriceOverflow,
52    /// Result overflowed uint160.
53    #[error("Result overflowed MAX_UINT160")]
54    ResultOverflowedUint160,
55    /// Insufficient liquidity for the requested output.
56    #[error("NotEnoughLiquidity")]
57    InsufficientLiquidity,
58    /// Invalid liquidity (negative where non-negative is required).
59    #[error("InvalidLiquidity")]
60    InvalidLiquidity,
61    /// Safe cast overflow (value doesn't fit in target type).
62    #[error("SafeCastOverflow")]
63    SafeCastOverflow,
64}
65
66#[cfg(feature = "pyo3")]
67impl From<ClMathError> for PyErr {
68    fn from(err: ClMathError) -> Self {
69        Self::new::<PyValueError, _>(err.to_string())
70    }
71}
72
73impl From<TickMathError> for ClMathError {
74    fn from(err: TickMathError) -> Self {
75        match err {
76            TickMathError::InvalidTick(_) | TickMathError::SqrtRatioOutOfBounds { .. } => {
77                Self::InvalidPrice
78            }
79        }
80    }
81}
82
83#[cfg(feature = "pyo3")]
84impl From<TickMathError> for PyErr {
85    fn from(err: TickMathError) -> Self {
86        // Forward the Display impl so the Python error message matches
87        // the Rust error message exactly.
88        Self::new::<PyValueError, _>(err.to_string())
89    }
90}
91
92/// Errors that can occur during ABI decoding.
93#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum AbiDecodeError {
96    /// Empty types list provided.
97    #[error("Types list cannot be empty")]
98    EmptyTypesList,
99    /// Empty data provided for decoding.
100    #[error("Data cannot be empty")]
101    EmptyData,
102    /// Invalid array size in type string.
103    #[error("Invalid array size in type: {0}")]
104    InvalidArraySize(String),
105    /// Insufficient data provided for decoding.
106    #[error("Insufficient data: need {needed} bytes at offset {offset}, have {have} bytes")]
107    InsufficientData {
108        /// Number of bytes needed.
109        needed: usize,
110        /// Number of bytes available.
111        have: usize,
112        /// Offset where data was needed.
113        offset: usize,
114    },
115    /// Invalid offset value in encoded data.
116    #[error("Invalid offset: {0}")]
117    InvalidOffset(String),
118    /// Invalid length value in encoded data.
119    #[error("Invalid length: {0}")]
120    InvalidLength(String),
121    /// Unsupported or invalid ABI type.
122    #[error("Unsupported type: {0}")]
123    UnsupportedType(String),
124    /// Fixed-point types are not yet implemented.
125    #[error("Fixed-point types are not yet implemented")]
126    FixedPointNotImplemented,
127    /// Non-strict decoding mode is not yet implemented.
128    #[error("Non-strict decoding mode is not yet implemented")]
129    NonStrictNotImplemented,
130}
131
132#[cfg(feature = "pyo3")]
133impl From<AbiDecodeError> for PyErr {
134    fn from(err: AbiDecodeError) -> Self {
135        // Preserve error context by forwarding the Display impl
136        Self::new::<PyValueError, _>(err.to_string())
137    }
138}
139
140/// Errors that can occur during address operations.
141#[derive(Debug, thiserror::Error)]
142#[non_exhaustive]
143pub enum AddressError {
144    /// Invalid address string format.
145    #[error("Invalid address string: {0}")]
146    InvalidAddress(String),
147    /// Address bytes must be exactly 20 bytes.
148    #[error("Address must be exactly 20 bytes, got {0} bytes")]
149    InvalidByteLength(usize),
150}
151
152#[cfg(feature = "pyo3")]
153impl From<AddressError> for PyErr {
154    fn from(err: AddressError) -> Self {
155        // Preserve original error message with full context
156        Self::new::<PyValueError, _>(err.to_string())
157    }
158}
159
160/// Errors that can occur during provider operations.
161#[derive(Debug, thiserror::Error, Clone)]
162#[non_exhaustive]
163pub enum ProviderError {
164    /// Request timeout.
165    #[error("Request timeout: {message}")]
166    Timeout { message: String },
167
168    /// Rate limit exceeded.
169    #[error("Rate limited: {message}")]
170    RateLimited { message: String },
171
172    /// Connection failed.
173    #[error("Connection failed: {message}")]
174    ConnectionFailed { message: String },
175
176    /// Invalid response from RPC.
177    #[error("Invalid response: {message}")]
178    InvalidResponse { message: String },
179
180    /// RPC error with code and message.
181    #[error("RPC error: {code} - {message}")]
182    RpcError { code: i64, message: String },
183
184    /// An EVM execution revert reported by the provider (`eth_call`/
185    /// `eth_estimateGas`). Replaces the string-scraping the Python adapter seam
186    /// (`degenbot.provider.alloy_errors`) used to perform: the JSON-RPC error
187    /// code (`-32000` Geth/Alchemy, `-3` Anvil, etc.) is preserved alongside
188    /// the message so the FFI layer can raise the degenbot-owned
189    /// `ContractLogicError` structurally rather than by marker substring.
190    #[error("execution reverted: {message}")]
191    ExecutionReverted { code: i64, message: String },
192
193    /// Invalid block range.
194    #[error("Invalid block range: {from} to {to}")]
195    InvalidBlockRange { from: u64, to: u64 },
196
197    /// Invalid Ethereum address format.
198    #[error("Invalid address format: {address} - {reason}")]
199    InvalidAddress { address: String, reason: String },
200
201    /// Invalid topic format.
202    #[error("Invalid topic format: {topic} - {reason}")]
203    InvalidTopic { topic: String, reason: String },
204
205    /// Invalid parameters.
206    #[error("Invalid parameters: {message}")]
207    InvalidParams { message: String },
208
209    /// Anvil-specific error.
210    #[error("Anvil error: {message}")]
211    AnvilError { message: String },
212
213    /// Invalid ABI.
214    #[error("Invalid ABI: {message}")]
215    InvalidAbi { message: String },
216
217    /// Encoding error.
218    #[error("Encoding error: {message}")]
219    EncodingError { message: String },
220
221    /// Decoding error.
222    #[error("Decoding error: {message}")]
223    DecodingError { message: String },
224
225    /// Serialization error.
226    #[error("Serialization error: {message}")]
227    SerializationError { message: String },
228
229    /// Subscription not supported (e.g., HTTP transport).
230    #[error("Subscription not supported: {message}")]
231    SubscriptionNotSupported { message: String },
232
233    /// The endpoint serves a different chain than the one it was bound to.
234    ///
235    /// Binding an endpoint to a chain reads `eth_chainId` once and refuses a
236    /// disagreement here, before any pool or token I/O runs against a node
237    /// that is not the chain the session declared. Not retryable: the same
238    /// endpoint will keep answering with the same chain.
239    #[error("endpoint {endpoint} reports chain id {actual}, not the expected {expected}")]
240    ChainMismatch {
241        /// The chain the endpoint was bound to.
242        expected: u64,
243        /// The chain the endpoint actually serves.
244        actual: u64,
245        /// The endpoint that was bound.
246        endpoint: String,
247    },
248
249    /// Other error.
250    #[error("{message}")]
251    Other { message: String },
252}
253
254impl ProviderError {
255    /// Returns `true` if this error is retryable (rate-limited, timeout, connection failure).
256    ///
257    /// Used by the retry-with-backoff loop to decide whether to sleep and retry
258    /// or surface the error immediately.
259    #[must_use]
260    pub const fn is_retryable(&self) -> bool {
261        matches!(
262            self,
263            Self::RateLimited { .. } | Self::Timeout { .. } | Self::ConnectionFailed { .. }
264        )
265    }
266}
267
268#[cfg(feature = "pyo3")]
269impl From<ProviderError> for PyErr {
270    fn from(err: ProviderError) -> Self {
271        let msg = format!("Provider error: {err}");
272        match err {
273            ProviderError::Timeout { .. } => Self::new::<pyo3::exceptions::PyTimeoutError, _>(msg),
274            ProviderError::ConnectionFailed { .. } => {
275                Self::new::<pyo3::exceptions::PyConnectionError, _>(msg)
276            }
277            // A wrong-chain endpoint is a configuration disagreement, so it
278            // raises as a ValueError — the shape every Python caller already
279            // handles for a misconfigured provider.
280            ProviderError::ChainMismatch { .. } => {
281                Self::new::<pyo3::exceptions::PyValueError, _>(msg)
282            }
283            ProviderError::SubscriptionNotSupported { .. }
284            | ProviderError::RateLimited { .. }
285            | ProviderError::RpcError { .. }
286            | ProviderError::ExecutionReverted { .. }
287            | ProviderError::SerializationError { .. }
288            | ProviderError::InvalidResponse { .. }
289            | ProviderError::AnvilError { .. }
290            | ProviderError::Other { .. }
291            | ProviderError::InvalidBlockRange { .. }
292            | ProviderError::InvalidAddress { .. }
293            | ProviderError::InvalidTopic { .. }
294            | ProviderError::InvalidParams { .. }
295            | ProviderError::InvalidAbi { .. }
296            | ProviderError::EncodingError { .. }
297            | ProviderError::DecodingError { .. } => {
298                Self::new::<pyo3::exceptions::PyRuntimeError, _>(msg)
299            }
300        }
301    }
302}
303
304/// Result type alias for provider operations.
305pub type ProviderResult<T> = Result<T, ProviderError>;
306
307/// Errors that can occur during contract ABI encoding/decoding.
308#[derive(Debug, thiserror::Error, Clone)]
309#[non_exhaustive]
310pub enum ContractError {
311    /// Invalid ABI type or value.
312    #[error("Invalid ABI: {message}")]
313    InvalidAbi {
314        /// Description of the ABI validation failure.
315        message: String,
316    },
317    /// Invalid address format.
318    #[error("Invalid address: {address} - {reason}")]
319    InvalidAddress {
320        /// The address string that failed validation.
321        address: String,
322        /// Why the address is invalid.
323        reason: String,
324    },
325    /// Encoding failure.
326    #[error("Encoding error: {message}")]
327    EncodingError {
328        /// Description of the encoding failure.
329        message: String,
330    },
331    /// Decoding failure.
332    #[error("Decoding error: {message}")]
333    DecodingError {
334        /// Description of the decoding failure.
335        message: String,
336    },
337}
338
339impl From<ContractError> for ProviderError {
340    fn from(err: ContractError) -> Self {
341        match err {
342            ContractError::InvalidAbi { message } => Self::InvalidAbi { message },
343            ContractError::InvalidAddress { address, reason } => {
344                Self::InvalidAddress { address, reason }
345            }
346            ContractError::EncodingError { message } => Self::EncodingError { message },
347            ContractError::DecodingError { message } => Self::DecodingError { message },
348        }
349    }
350}
351
352#[cfg(feature = "pyo3")]
353impl From<ContractError> for PyErr {
354    fn from(err: ContractError) -> Self {
355        Self::new::<PyValueError, _>(format!("{err}"))
356    }
357}
358
359impl From<crate::errors::AbiDecodeError> for ContractError {
360    fn from(err: crate::errors::AbiDecodeError) -> Self {
361        match err {
362            crate::errors::AbiDecodeError::UnsupportedType(msg) => {
363                Self::InvalidAbi { message: msg }
364            }
365            crate::errors::AbiDecodeError::InvalidLength(msg)
366            | crate::errors::AbiDecodeError::InvalidOffset(msg) => {
367                Self::DecodingError { message: msg }
368            }
369            crate::errors::AbiDecodeError::InsufficientData { .. } => Self::DecodingError {
370                message: err.to_string(),
371            },
372            other => Self::DecodingError {
373                message: other.to_string(),
374            },
375        }
376    }
377}
378
379/// Result type alias for contract ABI encoding/decoding operations.
380pub type ContractResult<T> = Result<T, ContractError>;