Skip to main content

cow_sdk_contracts/
errors.rs

1use cow_sdk_core::{Address, Cancelled, CoreError, ErrorClass, Redacted};
2use thiserror::Error;
3
4/// Errors returned by low-level `CoW` contract helpers.
5#[non_exhaustive]
6#[derive(Debug, Error)]
7pub enum ContractsError {
8    /// Core validation failed for an input value.
9    #[error("core validation error: {0}")]
10    Core(#[from] CoreError),
11    /// A long-running contracts operation was cancelled through a cooperative cancellation token.
12    #[error("contracts operation was cancelled")]
13    Cancelled,
14    /// A chain id is outside the supported `CoW` deployment set.
15    #[error("unsupported chain id: {0}")]
16    UnsupportedChain(u64),
17    /// No canonical deployment of the named contract is registered for the
18    /// chain/environment pair.
19    #[error("no {contract} deployment registered for chain {chain_id}")]
20    DeploymentNotFound {
21        /// Contract whose deployment is missing.
22        contract: &'static str,
23        /// Chain id with no registered deployment.
24        chain_id: u64,
25    },
26    /// An order UID had the wrong encoded byte length.
27    #[error("invalid order UID length: expected 56 bytes, got {actual}")]
28    InvalidOrderUidLength {
29        /// Actual decoded byte length.
30        actual: usize,
31    },
32    /// A signing-scheme discriminator was not recognized.
33    #[error("unsupported signing scheme value: {0}")]
34    UnsupportedSigningScheme(u8),
35    /// An order-kind or token-balance marker decoded from an on-chain
36    /// `OrderPlacement` event did not match any canonical `GPv2` label hash.
37    #[error("unrecognized GPv2 order marker: {0}")]
38    UnknownOrderMarker(alloy_primitives::B256),
39    /// An on-chain event log carried a topic set that did not match the
40    /// expected event signature hash or indexed-parameter arity.
41    #[error("unexpected event log topics for {event}")]
42    UnexpectedEventTopics {
43        /// Event whose topic set failed validation.
44        event: &'static str,
45    },
46    /// An encoded EIP-1271 signature payload was malformed.
47    #[error("invalid EIP-1271 signature payload")]
48    InvalidEip1271SignatureData,
49    /// A verifier address did not have contract bytecode.
50    #[error("EIP-1271 verifier {verifier} does not expose contract code")]
51    UnsupportedEip1271Verifier {
52        /// Verifier address that lacked contract code.
53        verifier: Address,
54    },
55    /// The provider failed during an EIP-1271 operation.
56    #[error("provider error during EIP-1271 {operation}: {message}")]
57    Eip1271Provider {
58        /// Operation being performed.
59        operation: &'static str,
60        /// Provider error message.
61        message: Redacted<String>,
62    },
63    /// The EIP-1271 call response could not be decoded.
64    #[error("malformed EIP-1271 response: {response}")]
65    MalformedEip1271Response {
66        /// Raw response that failed decoding.
67        response: Redacted<String>,
68    },
69    /// The verifier returned an unexpected EIP-1271 magic value.
70    #[error(
71        "unexpected EIP-1271 magic value: expected 0x{}, got 0x{}",
72        alloy_primitives::hex::encode(expected),
73        alloy_primitives::hex::encode(actual)
74    )]
75    Eip1271MagicValueMismatch {
76        /// Expected 4-byte magic value.
77        expected: [u8; 4],
78        /// Actual 4-byte magic value returned by the verifier.
79        actual: [u8; 4],
80    },
81    /// Contract orders cannot use the zero address as a receiver.
82    #[error("receiver cannot be address(0)")]
83    ZeroReceiver,
84    /// Provider operation failed outside the EIP-1271 helpers.
85    #[error("provider error during {operation}: {message}")]
86    Provider {
87        /// Failed provider operation.
88        operation: &'static str,
89        /// Provider error message.
90        message: Redacted<String>,
91    },
92    /// ABI encoding or decoding failed through the `alloy-sol-types` surface.
93    #[error("ABI error: {0}")]
94    Abi(#[from] alloy_sol_types::Error),
95    /// Hex decoding failed for a named field; the underlying
96    /// [`alloy_primitives::hex::FromHexError`] is preserved in the error-source chain.
97    #[error("hex decode error for field `{field}`: {source}")]
98    DecodeHex {
99        /// Public field or payload name that failed to decode.
100        field: &'static str,
101        /// Typed hex-decode error sourced from the decoder.
102        #[source]
103        source: alloy_primitives::hex::FromHexError,
104    },
105    /// A hexadecimal payload was not `0x`-prefixed.
106    #[error("field `{field}` must be 0x-prefixed hexadecimal data")]
107    InvalidHexPrefix {
108        /// Public field or payload name that failed the prefix check.
109        field: &'static str,
110    },
111    /// A hexadecimal payload decoded to an unexpected byte length.
112    #[error(
113        "field `{field}` must decode to {expected} bytes, got {actual} byte(s) after 0x prefix"
114    )]
115    InvalidDecodedLength {
116        /// Public field or payload name that failed the length check.
117        field: &'static str,
118        /// Expected decoded byte length.
119        expected: usize,
120        /// Actual decoded byte length.
121        actual: usize,
122    },
123    /// A hexadecimal payload exceeded the maximum decoded byte length
124    /// permitted for its field.
125    #[error("field `{field}` exceeds the maximum of {max_bytes} decoded bytes")]
126    FieldTooLarge {
127        /// Public field or payload name that exceeded the limit.
128        field: &'static str,
129        /// Maximum decoded byte length permitted for the field.
130        max_bytes: usize,
131    },
132    /// JSON serialization or decoding failed.
133    ///
134    /// Only the serde failure category and the structural position are
135    /// surfaced. The raw `serde_json::Error` rendering can echo bytes from a
136    /// decoded payload, so the conversion drops it (ADR 0025); the
137    /// `category`/`line`/`column` triple is the safe structural diagnostic,
138    /// mirroring [`cow_sdk_orderbook::OrderbookError::Serialization`].
139    ///
140    /// [`cow_sdk_orderbook::OrderbookError::Serialization`]: https://docs.rs/cow-sdk-orderbook
141    #[error("serialization error ({category}) at line {line} column {column}")]
142    Serialization {
143        /// serde failure category: `"syntax"`, `"data"`, `"eof"`, or `"io"`.
144        category: &'static str,
145        /// 1-based line where decoding failed, or `0` when the position is unknown.
146        line: usize,
147        /// 1-based column where decoding failed, or `0` when the position is unknown.
148        column: usize,
149    },
150    /// Signature byte length is not the required 65.
151    #[error("invalid signature length: expected 65 bytes, got {actual}")]
152    InvalidSignatureLength {
153        /// Observed byte length.
154        actual: usize,
155    },
156    /// Recovery byte is outside the accepted {0, 1, 27, 28} set.
157    #[error("invalid signature recovery byte: expected v in {{0, 1, 27, 28}}, got {value}")]
158    InvalidSignatureRecoveryByte {
159        /// Rejected recovery byte value.
160        value: u8,
161    },
162    /// A non-ECDSA signature variant was passed to an ECDSA-only helper.
163    #[error("signature scheme is not ECDSA")]
164    SignatureSchemeNotEcdsa,
165    /// ECDSA public-key recovery failed.
166    #[error("ECDSA signature recovery failed: {message}")]
167    SignatureRecovery {
168        /// Sanitized recovery failure description from the backend.
169        message: Redacted<String>,
170    },
171    /// Composable TWAP inputs failed client-side validation.
172    #[cfg(feature = "composable")]
173    #[error("composable TWAP validation error: {0}")]
174    TwapValidation(#[from] crate::composable::TwapValidationError),
175    /// A composable conditional-order multiplexer operation failed.
176    #[cfg(feature = "composable")]
177    #[error("composable multiplexer error: {0}")]
178    Multiplexer(#[from] crate::composable::MultiplexerError),
179}
180
181impl From<Cancelled> for ContractsError {
182    fn from(_: Cancelled) -> Self {
183        Self::Cancelled
184    }
185}
186
187impl ContractsError {
188    /// Returns the coarse-grained [`ErrorClass`] for this error.
189    #[must_use]
190    pub const fn class(&self) -> ErrorClass {
191        match self {
192            Self::Core(error) => error.class(),
193            Self::Cancelled => ErrorClass::Cancelled,
194            // Caller-supplied input that failed a client-side shape or range
195            // check classifies as validation.
196            Self::UnsupportedChain(_)
197            | Self::DeploymentNotFound { .. }
198            | Self::InvalidOrderUidLength { .. }
199            | Self::InvalidHexPrefix { .. }
200            | Self::InvalidDecodedLength { .. }
201            | Self::FieldTooLarge { .. }
202            | Self::InvalidSignatureLength { .. }
203            | Self::InvalidSignatureRecoveryByte { .. }
204            | Self::ZeroReceiver => ErrorClass::Validation,
205            // Composable TWAP/multiplexer errors are client-side input checks.
206            #[cfg(feature = "composable")]
207            Self::TwapValidation(_) | Self::Multiplexer(_) => ErrorClass::Validation,
208            // Serialization, ABI, hex-decode, and on-chain event/marker decode
209            // failures are data round-trip invariants, matching the
210            // `CoreError` serialization classification.
211            Self::Serialization { .. }
212            | Self::Abi(_)
213            | Self::DecodeHex { .. }
214            | Self::UnknownOrderMarker(_)
215            | Self::UnexpectedEventTopics { .. } => ErrorClass::Internal,
216            // EIP-1271 verification, provider interaction, ECDSA recovery,
217            // signing-scheme classification, and any future additive variant
218            // classify as the contracts crate's signing-edge bucket.
219            _ => ErrorClass::Signing,
220        }
221    }
222}
223
224impl From<serde_json::Error> for ContractsError {
225    /// Captures only the serde failure category and structural position.
226    ///
227    /// The raw `serde_json::Error` rendering can echo bytes from a decoded
228    /// payload, so it is intentionally dropped here (ADR 0025); only the
229    /// `category`/`line`/`column` triple is retained.
230    fn from(error: serde_json::Error) -> Self {
231        Self::Serialization {
232            category: cow_sdk_core::serialization_error_category(&error),
233            line: error.line(),
234            column: error.column(),
235        }
236    }
237}