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}