1use std::ops::Deref;
2
3use alloy_primitives::keccak256;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::{AppDataDoc, AppDataError, AppDataInfo, app_data_hex_to_cid};
8
9pub const APP_DATA_MAX_BYTES: usize = 8192;
15
16pub const APP_DATA_APPROACHING_LIMIT_RATIO: f64 = 0.75;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[non_exhaustive]
34pub struct AppDataValidated {
35 pub info: AppDataInfo,
37 pub validation: AppDataValidation,
39}
40
41impl AppDataValidated {
42 #[must_use]
44 pub const fn new(info: AppDataInfo, validation: AppDataValidation) -> Self {
45 Self { info, validation }
46 }
47}
48
49impl Deref for AppDataValidated {
50 type Target = AppDataInfo;
51
52 fn deref(&self) -> &Self::Target {
53 &self.info
54 }
55}
56
57#[non_exhaustive]
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct AppDataValidation {
66 pub bytes_used: usize,
69 pub warnings: Vec<AppDataWarning>,
73}
74
75#[non_exhaustive]
84#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
85#[serde(tag = "kind", rename_all = "camelCase")]
86pub enum AppDataWarning {
87 #[serde(rename_all = "camelCase")]
92 ApproachingSizeLimit {
93 bytes_used: usize,
95 max_bytes: usize,
97 },
98}
99
100pub trait AppDataSource {
102 fn into_document_and_content(
111 self,
112 deterministic: bool,
113 ) -> Result<(AppDataDoc, String), AppDataError>;
114}
115
116impl AppDataSource for &AppDataDoc {
117 fn into_document_and_content(
118 self,
119 deterministic: bool,
120 ) -> Result<(AppDataDoc, String), AppDataError> {
121 let content = if deterministic {
122 stringify_deterministic(self)?
123 } else {
124 serde_json::to_string(self).map_err(AppDataError::from)?
125 };
126 Ok((self.clone(), content))
127 }
128}
129
130impl AppDataSource for AppDataDoc {
131 fn into_document_and_content(
132 self,
133 deterministic: bool,
134 ) -> Result<(AppDataDoc, String), AppDataError> {
135 let content = if deterministic {
136 stringify_deterministic(&self)?
137 } else {
138 serde_json::to_string(&self).map_err(AppDataError::from)?
139 };
140 Ok((self, content))
141 }
142}
143
144impl AppDataSource for &str {
145 fn into_document_and_content(
146 self,
147 _deterministic: bool,
148 ) -> Result<(AppDataDoc, String), AppDataError> {
149 let document: Value = serde_json::from_str(self).map_err(AppDataError::from)?;
150 Ok((document, self.to_string()))
151 }
152}
153
154impl AppDataSource for String {
155 fn into_document_and_content(
156 self,
157 _deterministic: bool,
158 ) -> Result<(AppDataDoc, String), AppDataError> {
159 let document: Value = serde_json::from_str(&self).map_err(AppDataError::from)?;
160 Ok((document, self))
161 }
162}
163
164pub fn app_data_info(source: impl AppDataSource) -> Result<AppDataValidated, AppDataError> {
182 let (document, app_data_content) = source.into_document_and_content(true)?;
183 ensure_document_under_size_limit(&app_data_content, APP_DATA_MAX_BYTES)?;
184 crate::schema::validate_app_data_doc(&document)?;
188
189 let bytes_used = app_data_content.len();
190 let digest = keccak256(app_data_content.as_bytes());
191 let app_data_hex = alloy_primitives::hex::encode_prefixed(digest);
192 let cid = app_data_hex_to_cid(&app_data_hex)?;
193
194 let info = AppDataInfo {
195 cid,
196 app_data_content,
197 app_data_hex,
198 };
199
200 let mut warnings = Vec::new();
201 if approaching_size_limit(bytes_used, APP_DATA_MAX_BYTES) {
202 warnings.push(AppDataWarning::ApproachingSizeLimit {
203 bytes_used,
204 max_bytes: APP_DATA_MAX_BYTES,
205 });
206 }
207
208 Ok(AppDataValidated {
209 info,
210 validation: AppDataValidation {
211 bytes_used,
212 warnings,
213 },
214 })
215}
216
217fn approaching_size_limit(bytes_used: usize, max_bytes: usize) -> bool {
218 #[allow(
219 clippy::cast_precision_loss,
220 clippy::cast_possible_truncation,
221 clippy::cast_sign_loss,
222 reason = "byte-size and ratio multiplication produces values that fit back inside usize with the floor the test contract requires"
223 )]
224 let threshold = (max_bytes as f64 * APP_DATA_APPROACHING_LIMIT_RATIO) as usize;
225 bytes_used >= threshold
226}
227
228pub fn stringify_deterministic(value: &AppDataDoc) -> Result<String, AppDataError> {
244 serde_jcs::to_string(value).map_err(AppDataError::from)
245}
246
247const fn ensure_document_under_size_limit(
248 content: &str,
249 max_bytes: usize,
250) -> Result<(), AppDataError> {
251 let actual = content.len();
252 if actual > max_bytes {
253 return Err(AppDataError::TooLarge {
254 actual_bytes: actual,
255 max_bytes,
256 });
257 }
258 Ok(())
259}
260
261pub fn app_data_info_hex(source: impl AppDataSource) -> Result<String, AppDataError> {
267 Ok(app_data_info(source)?.info.app_data_hex)
268}
269
270pub fn app_data_cid(source: impl AppDataSource) -> Result<String, AppDataError> {
276 Ok(app_data_info(source)?.info.cid)
277}
278
279pub fn app_data_content(source: impl AppDataSource) -> Result<String, AppDataError> {
285 Ok(app_data_info(source)?.info.app_data_content)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use serde_json::{Value, json};
292
293 #[test]
294 fn stringify_deterministic_orders_keys_without_corrupting_arrays() {
295 let document = json!({
296 "version": "0.7.0",
297 "metadata": {
298 "nested": {
299 "b": 2,
300 "a": 1
301 },
302 "array": [3, 2, 1]
303 },
304 "appCode": "CoW Swap"
305 });
306
307 assert_eq!(
308 stringify_deterministic(&document).unwrap(),
309 r#"{"appCode":"CoW Swap","metadata":{"array":[3,2,1],"nested":{"a":1,"b":2}},"version":"0.7.0"}"#
310 );
311 }
312
313 #[test]
314 fn string_sources_preserve_the_original_content_and_document_shape() {
315 let raw = r#"{"metadata":{"b":2,"a":1},"version":"0.7.0","appCode":"CoW Swap"}"#;
316 let expected = serde_json::from_str::<Value>(raw).unwrap();
317
318 let (borrowed_document, borrowed_content) =
319 <&str as AppDataSource>::into_document_and_content(raw, false).unwrap();
320 assert_eq!(borrowed_document, expected);
321 assert_eq!(borrowed_content, raw);
322
323 let owned = raw.to_owned();
324 let (owned_document, owned_content) =
325 owned.clone().into_document_and_content(false).unwrap();
326 assert_eq!(owned_document, expected);
327 assert_eq!(owned_content, owned);
328 }
329
330 #[test]
331 fn accessors_match_the_primary_app_data_info_result() {
332 let document = json!({
333 "appCode": "CoW Swap",
334 "metadata": {
335 "quote": {
336 "version": "0.2.0",
337 "slippageBips": "5"
338 }
339 },
340 "version": "0.7.0"
341 });
342
343 let info = app_data_info(&document).unwrap();
344
345 assert_eq!(app_data_info_hex(&document).unwrap(), info.app_data_hex);
346 assert_eq!(app_data_cid(&document).unwrap(), info.cid);
347 assert_eq!(app_data_content(&document).unwrap(), info.app_data_content);
348 }
349}