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
//! Error and `Result` types for the `cow-rs` crate.
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::{chain::UnsupportedChain, signature::SignatureError, subgraph::SubgraphError};
/// 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.
#[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_for`] 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,
},
}
/// 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");
}
}