cow_sdk_app_data/errors.rs
1use cow_sdk_core::{Cancelled, ErrorClass, Redacted, TransportErrorClass, ValidationReason};
2use serde::ser::SerializeMap;
3use serde::{Serialize, Serializer};
4use thiserror::Error;
5
6/// Errors returned by app-data generation, validation, transport, and CID helpers.
7#[non_exhaustive]
8#[derive(Debug, Error)]
9pub enum AppDataError {
10 /// The supplied app-data hash was not valid `0x`-prefixed 32-byte hex.
11 #[error("invalid app data hex")]
12 InvalidAppDataHex,
13 /// The supplied CID was malformed or unsupported.
14 #[error("invalid cid format")]
15 InvalidCid,
16 /// The supplied schema version did not match the expected `major.minor.patch` format.
17 #[error("app-data version {0} is not a valid version")]
18 InvalidSchemaVersion(Redacted<String>),
19 /// The app-data document did not contain a string `version` field.
20 #[error("app-data document is missing string field `version`")]
21 MissingSchemaVersion,
22 /// JSON serialization or decoding failed.
23 ///
24 /// Only the serde failure category and the structural position are
25 /// surfaced. The raw `serde_json::Error` rendering can echo bytes from a
26 /// decoded document or response body, so the conversion drops it
27 /// (ADR 0025); the `category`/`line`/`column` triple is the safe structural
28 /// diagnostic, mirroring `cow_sdk_orderbook::OrderbookError::Serialization`.
29 #[error("json error ({category}) at line {line} column {column}")]
30 Serialization {
31 /// serde failure category: `"syntax"`, `"data"`, `"eof"`, or `"io"`.
32 category: &'static str,
33 /// 1-based line where decoding failed, or `0` when the position is unknown.
34 line: usize,
35 /// 1-based column where decoding failed, or `0` when the position is unknown.
36 column: usize,
37 },
38 /// The supplied app-data document failed semantic validation.
39 #[error("invalid appData field `{field}`: {reason}")]
40 InvalidAppDataProvided {
41 /// Public field name that failed validation.
42 field: &'static str,
43 /// Canonical validation-failure mode.
44 reason: ValidationReason,
45 },
46 /// A partner-fee policy failed semantic validation against the
47 /// documented basis-point bounds or recipient preconditions.
48 #[error("invalid partner-fee field `{field}`: {reason}")]
49 InvalidPartnerFee {
50 /// Public field name that failed validation.
51 field: &'static str,
52 /// Canonical validation-failure mode.
53 reason: ValidationReason,
54 },
55 /// A flash-loan hint failed semantic validation against the documented
56 /// bounds for `amount` or the non-zero-address preconditions on
57 /// `liquidityProvider`, `protocolAdapter`, `receiver`, and `token`.
58 #[error("invalid flashloan-hints field `{field}`: {reason}")]
59 InvalidFlashloanHints {
60 /// Public field name that failed validation, spelled as the
61 /// camelCase wire key for stable error observability.
62 field: &'static str,
63 /// Canonical validation-failure mode.
64 reason: ValidationReason,
65 },
66 /// CID or digest calculation failed with a typed underlying error
67 /// preserved through the error-source chain.
68 ///
69 /// The boxed source is intentionally not rendered into `Display` or
70 /// `Serialize`: a future hashing or CID backend could embed
71 /// caller-derived bytes in its message, so only the stable operation
72 /// label is surfaced (ADR 0025). Callers that need the precise failure
73 /// walk [`std::error::Error::source`].
74 #[error("appDataHex calculation failed")]
75 Calculation {
76 /// Typed source error returned by the underlying hashing or CID
77 /// crate. Boxed as a trait object so the variant can carry either
78 /// a [`cid`]-crate or a [`multihash`]-crate failure without
79 /// widening the enum surface. Reachable through
80 /// [`std::error::Error::source`] for callers that deliberately cross
81 /// the redaction boundary.
82 #[source]
83 source: Box<dyn std::error::Error + Send + Sync + 'static>,
84 },
85 /// Fetch-transport configuration or execution failed.
86 #[error("transport error ({class}): {detail}")]
87 Transport {
88 /// Classification of the underlying REST-transport failure.
89 class: TransportErrorClass,
90 /// Redacted detail message sourced from the transport layer.
91 detail: Redacted<String>,
92 },
93 /// A long-running app-data operation was cancelled through a cooperative cancellation token.
94 #[error("app-data operation was cancelled")]
95 Cancelled,
96 /// The stringified app-data document exceeded the configured size ceiling.
97 #[error("app-data document is {actual_bytes} bytes which exceeds the {max_bytes}-byte limit")]
98 TooLarge {
99 /// Size of the stringified document in bytes.
100 actual_bytes: usize,
101 /// Configured size ceiling in bytes.
102 max_bytes: usize,
103 },
104}
105
106impl From<Cancelled> for AppDataError {
107 fn from(_: Cancelled) -> Self {
108 Self::Cancelled
109 }
110}
111
112impl From<serde_json::Error> for AppDataError {
113 /// Captures only the serde failure category and structural position.
114 ///
115 /// The raw `serde_json::Error` rendering can echo bytes from a decoded
116 /// document or response body, so it is intentionally dropped here
117 /// (ADR 0025); only the `category`/`line`/`column` triple is retained.
118 fn from(error: serde_json::Error) -> Self {
119 Self::Serialization {
120 category: cow_sdk_core::serialization_error_category(&error),
121 line: error.line(),
122 column: error.column(),
123 }
124 }
125}
126
127impl AppDataError {
128 /// Returns the coarse-grained [`ErrorClass`] for this error.
129 #[must_use]
130 pub const fn class(&self) -> ErrorClass {
131 match self {
132 Self::InvalidAppDataHex
133 | Self::InvalidCid
134 | Self::InvalidSchemaVersion(_)
135 | Self::MissingSchemaVersion
136 | Self::InvalidAppDataProvided { .. }
137 | Self::TooLarge { .. } => ErrorClass::Validation,
138 Self::Transport { .. } => ErrorClass::Transport,
139 Self::Cancelled => ErrorClass::Cancelled,
140 // Serialization, Calculation, and partner-fee / flashloan validation
141 // failures plus any future additive variants classify as internal.
142 _ => ErrorClass::Internal,
143 }
144 }
145}
146
147impl Serialize for AppDataError {
148 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149 let mut map = serializer.serialize_map(None)?;
150
151 match self {
152 Self::InvalidAppDataHex => {
153 map.serialize_entry("type", "InvalidAppDataHex")?;
154 }
155 Self::InvalidCid => {
156 map.serialize_entry("type", "InvalidCid")?;
157 }
158 Self::InvalidSchemaVersion(version) => {
159 map.serialize_entry("type", "InvalidSchemaVersion")?;
160 map.serialize_entry("version", version)?;
161 }
162 Self::MissingSchemaVersion => {
163 map.serialize_entry("type", "MissingSchemaVersion")?;
164 }
165 Self::Serialization {
166 category,
167 line,
168 column,
169 } => {
170 map.serialize_entry("type", "Serialization")?;
171 map.serialize_entry("category", category)?;
172 map.serialize_entry("line", line)?;
173 map.serialize_entry("column", column)?;
174 }
175 Self::InvalidAppDataProvided { field, reason } => {
176 map.serialize_entry("type", "InvalidAppDataProvided")?;
177 map.serialize_entry("field", field)?;
178 map.serialize_entry("reason", &reason.to_string())?;
179 }
180 Self::InvalidPartnerFee { field, reason } => {
181 map.serialize_entry("type", "InvalidPartnerFee")?;
182 map.serialize_entry("field", field)?;
183 map.serialize_entry("reason", &reason.to_string())?;
184 }
185 Self::InvalidFlashloanHints { field, reason } => {
186 map.serialize_entry("type", "InvalidFlashloanHints")?;
187 map.serialize_entry("field", field)?;
188 map.serialize_entry("reason", &reason.to_string())?;
189 }
190 Self::Calculation { .. } => {
191 map.serialize_entry("type", "Calculation")?;
192 // The boxed source is not serialized: it could embed
193 // caller-derived bytes. Only the stable label is emitted
194 // (ADR 0025); callers walk `Error::source` for the detail.
195 map.serialize_entry("message", "appDataHex calculation failed")?;
196 }
197 Self::Transport { class, detail } => {
198 map.serialize_entry("type", "Transport")?;
199 map.serialize_entry("class", &class.to_string())?;
200 map.serialize_entry("detail", detail)?;
201 }
202 Self::Cancelled => {
203 map.serialize_entry("type", "Cancelled")?;
204 }
205 Self::TooLarge {
206 actual_bytes,
207 max_bytes,
208 } => {
209 map.serialize_entry("type", "TooLarge")?;
210 map.serialize_entry("actualBytes", actual_bytes)?;
211 map.serialize_entry("maxBytes", max_bytes)?;
212 }
213 }
214
215 map.end()
216 }
217}