Skip to main content

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}