1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
//! Error and `Result` types for the `cow-rs` crate.
use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "subgraph")]
use crate::subgraph::SubgraphError;
use crate::{chain::UnsupportedChain, signature::SignatureError};
/// Crate-wide `Result` alias.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Top-level error type for `cow-rs`.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// HTTP transport, redirect, body or response error.
#[error("transport error: {0}")]
Transport(#[from] reqwest::Error),
/// JSON serialisation or deserialisation error.
#[error("serde error: {0}")]
Serde(#[from] serde_json::Error),
/// URL build error.
#[error("url error: {0}")]
Url(#[from] url::ParseError),
/// Chain id not supported by [`crate::Chain`].
#[error(transparent)]
UnsupportedChain(#[from] UnsupportedChain),
/// The CoW orderbook responded with a structured error envelope.
#[error("orderbook error ({}{}): {}",
api.error_type,
api.data.as_ref().map(|_| ", +data").unwrap_or(""),
api.description,
)]
OrderbookApi {
/// HTTP status returned with the error.
status: reqwest::StatusCode,
/// Decoded `ApiError` body.
api: ApiError,
},
/// The orderbook returned a non-2xx status with an unparseable body.
#[error("unexpected orderbook status {status}: {body}")]
UnexpectedStatus {
/// HTTP status.
status: reqwest::StatusCode,
/// Raw body verbatim.
body: String,
},
/// The CoW Protocol subgraph returned GraphQL errors or an
/// unrecognised envelope.
#[cfg(feature = "subgraph")]
#[error(transparent)]
Subgraph(#[from] SubgraphError),
/// Signature parsing, signing, or recovery failed.
#[error(transparent)]
Signature(#[from] SignatureError),
/// The submission-side adjustment `sellAmount + feeAmount` overflowed
/// the U256 range. Real quotes never come anywhere close, but the
/// orderbook would silently accept the saturated value and produce a
/// different on-chain order than the user signed.
#[error("quote amount overflow: sell={sell} + fee={fee} exceeds U256")]
QuoteAmountOverflow {
/// `quote.sellAmount` before adjustment.
sell: alloy_primitives::U256,
/// `quote.feeAmount` we tried to fold into it.
fee: alloy_primitives::U256,
},
/// An [`crate::OrderCreation`] field did not satisfy the orderbook's
/// preconditions; surfaced locally so the body is never shipped.
#[error("invalid OrderCreation: {field} {reason}")]
OrderCreationInvalid {
/// Field that failed validation.
field: &'static str,
/// Why it failed.
reason: &'static str,
},
/// A field on the orderbook's quote response did not match the
/// caller's [`crate::QuoteRequest`]. Raised by
/// [`crate::OrderQuoteResponse::to_signed_order_data`] before any
/// `OrderData` is returned, so a hostile orderbook cannot trick the
/// caller into signing an order with a swapped buy token, recipient,
/// or app-data digest.
#[error("quote field {field} mismatch: requested {requested}, returned {returned}")]
QuoteFieldMismatch {
/// Which field of the response disagreed with the request.
field: &'static str,
/// What the caller asked for, formatted via `Display`.
requested: String,
/// What the orderbook returned, formatted via `Display`.
returned: String,
},
/// An HTTP response body exceeded the configured cap before being
/// fully read. Defends against a hostile orderbook streaming a
/// multi-GB body to exhaust the SDK's memory.
#[error("orderbook response exceeded {max} byte cap")]
ResponseTooLarge {
/// Maximum byte length the SDK accepts for this endpoint.
max: usize,
},
/// `protocol_fee_bps` could not be parsed as a non-negative decimal
/// with at most 5 fractional digits. The orderbook serialises the
/// field as a JSON string (e.g. `"0.3"`); this variant fires when
/// the value is malformed or carries more precision than the
/// internal `bps * 100_000` scale can represent.
#[error("invalid protocol_fee_bps {value:?}: {reason}")]
InvalidProtocolFeeBps {
/// The string the caller (or orderbook) passed in.
value: String,
/// Why parsing failed.
reason: &'static str,
},
/// A `quote.sellAmount` of zero made [`crate::quote_amounts::compute`]
/// unable to project network costs into the buy currency. This is a
/// degenerate quote (no input to sell) and never appears for orders
/// the orderbook would settle; we refuse it explicitly so the fee
/// math cannot divide by zero downstream.
#[error("quote sellAmount is zero, network cost projection undefined")]
QuoteSellAmountZero,
/// A [`crate::quote_amounts::compute`] intermediate overflowed or
/// underflowed before reaching the signed [`crate::OrderData`].
/// Mirrors the fail-closed contract of [`Self::QuoteAmountOverflow`]
/// for the full fee-composition path: a hostile or malformed
/// orderbook response that would push `sellAmount`, `buyAmount`,
/// `feeAmount`, or `protocolFeeBps` into a U256-saturating
/// computation is rejected before any saturated bytes are folded
/// into a signature. `stage` labels the leg that failed (e.g.
/// `"before_all_fees.buy"`, `"protocol_fee.mul_div"`,
/// `"after_slippage.sell"`) so the offending input is greppable.
#[error("quote fee math overflow at {stage}")]
QuoteFeeMathOverflow {
/// Name of the projection leg whose checked arithmetic failed.
stage: &'static str,
},
/// The keccak256 of an [`crate::AppDataDocument`]'s `fullAppData`
/// bytes did not match the [`crate::AppDataHash`] it was paired with.
/// Raised by [`crate::OrderBookApi::get_app_data`] when the orderbook
/// serves a document that does not hash to the requested digest, and
/// by [`crate::OrderBookApi::put_app_data`] before issuing a pin
/// whose payload would be rejected server-side. The signed order
/// commits only to the digest, so a divergent body would let
/// downstream wallets, bots, or UIs display or validate metadata
/// different from what the order actually commits to.
#[error("app-data hash mismatch: expected {expected}, computed {computed}")]
AppDataHashMismatch {
/// The hash the caller asked for or paired with the document.
expected: String,
/// `keccak256(document.fullAppData.as_bytes())` as actually
/// computed.
computed: String,
},
}
/// Structured error envelope returned by the CoW orderbook for 4xx / 5xx
/// responses. Mirrors the `Error` schema declared by the orderbook OpenAPI.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ApiError {
/// Short machine-readable code (e.g. `"InvalidSignature"`).
#[serde(rename = "errorType")]
pub error_type: String,
/// Human-readable description.
pub description: String,
/// Optional structured data attached by the orderbook.
#[serde(default)]
pub data: Option<serde_json::Value>,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.error_type, self.description)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_error_round_trips_minimal_body() {
let json = serde_json::json!({
"errorType": "InsufficientFee",
"description": "fee too low",
});
let parsed: ApiError = serde_json::from_value(json).unwrap();
assert_eq!(parsed.error_type, "InsufficientFee");
assert_eq!(parsed.description, "fee too low");
assert!(parsed.data.is_none());
}
#[test]
fn api_error_keeps_data_field_when_present() {
let json = serde_json::json!({
"errorType": "QuoteNotFound",
"description": "no quote for token pair",
"data": { "fee_amount": "1234" },
});
let parsed: ApiError = serde_json::from_value(json).unwrap();
assert!(parsed.data.is_some());
assert_eq!(parsed.data.unwrap()["fee_amount"], "1234");
}
}