cow_rs/app_data/hash.rs
1//! Canonical JSON → `keccak256` hashing for `CoW` Protocol app-data.
2//!
3//! Every `CoW` Protocol order carries a 32-byte `appData` field that commits
4//! to a JSON document describing the order's intent, referral, hooks, and
5//! more. This module provides the functions that build that document,
6//! serialise it to deterministic JSON (sorted keys, no whitespace), and hash
7//! it with `keccak256`.
8//!
9//! # Key functions
10//!
11//! | Function | Use case |
12//! |---|---|
13//! | [`appdata_hex`] | Hash an existing [`AppDataDoc`] → [`B256`] |
14//! | [`build_order_app_data`] | Simple order → `0x`-prefixed hex string |
15//! | [`build_app_data_doc`] | Order with metadata → hex string |
16//! | [`build_app_data_doc_full`] | Order with metadata → `(json, hex)` |
17//! | [`appdata_json`] | Get the canonical JSON without hashing |
18//! | [`stringify_deterministic`] | Low-level sorted-key JSON serialiser |
19//! | [`merge_app_data_doc`] | Deep-merge two documents |
20
21use alloy_primitives::{B256, keccak256};
22use serde_json::Value;
23
24use crate::error::CowError;
25
26use super::types::{AppDataDoc, LATEST_APP_DATA_VERSION, Metadata};
27
28/// Serialise `doc` to canonical JSON with sorted keys, then return
29/// `keccak256(json_bytes)`.
30///
31/// The returned [`B256`] is the 32-byte digest used as the `appData` field in
32/// every [`UnsignedOrder`](crate::order_signing::types::UnsignedOrder).
33/// Internally, the document is first passed through [`stringify_deterministic`]
34/// (which sorts all object keys alphabetically and strips whitespace) before
35/// hashing, guaranteeing the same [`AppDataDoc`] always produces the same hash
36/// regardless of field insertion order.
37///
38/// Mirrors `appDataHex` from the `@cowprotocol/app-data` `TypeScript` package.
39///
40/// # Parameters
41///
42/// * `doc` — the [`AppDataDoc`] to hash. Only its JSON-serialisable fields contribute to the
43/// digest; `#[serde(skip)]` fields are excluded.
44///
45/// # Returns
46///
47/// A 32-byte [`B256`] containing the `keccak256` hash of the canonical JSON.
48///
49/// # Errors
50///
51/// Returns [`CowError::AppData`] if the document cannot be serialised to JSON
52/// (e.g. a custom `Serialize` impl fails).
53///
54/// # Example
55///
56/// ```
57/// use cow_rs::app_data::{AppDataDoc, appdata_hex};
58///
59/// let doc = AppDataDoc::new("MyDApp");
60/// let hash = appdata_hex(&doc).unwrap();
61/// // The hash is deterministic — calling again yields the same value.
62/// assert_eq!(hash, appdata_hex(&doc).unwrap());
63/// ```
64pub fn appdata_hex(doc: &AppDataDoc) -> Result<B256, CowError> {
65 let json = stringify_deterministic(doc)?;
66 Ok(keccak256(json.as_bytes()))
67}
68
69/// Build the `0x`-prefixed app-data hex string for an order identified by
70/// `app_code`.
71///
72/// This is the simplest entry point for generating the `appData` value needed
73/// by every `CoW` Protocol order. It creates an [`AppDataDoc`] with the given
74/// `app_code`, no extra metadata, and the latest schema version, then
75/// serialises it deterministically and returns the `keccak256` hex digest.
76///
77/// For orders that carry structured metadata (hooks, partner fees, UTM, …),
78/// use [`build_app_data_doc`] or [`build_app_data_doc_full`] instead.
79///
80/// Mirrors the simple-case path of `buildAppData` from the `TypeScript` SDK.
81///
82/// # Parameters
83///
84/// * `app_code` — application identifier embedded in the order metadata (e.g. `"CoW Swap"`,
85/// `"MyDApp"`). Must be ≤ 50 characters.
86///
87/// # Returns
88///
89/// A `0x`-prefixed, lowercase hex string of the 32-byte `keccak256` hash.
90///
91/// # Errors
92///
93/// Returns [`CowError::AppData`] if serialisation fails.
94///
95/// # Example
96///
97/// ```
98/// use cow_rs::app_data::build_order_app_data;
99///
100/// let hex = build_order_app_data("MyDApp").unwrap();
101/// assert!(hex.starts_with("0x"));
102/// assert_eq!(hex.len(), 66); // "0x" + 64 hex chars
103/// ```
104pub fn build_order_app_data(app_code: &str) -> Result<String, CowError> {
105 let doc = AppDataDoc::new(app_code);
106 let hash = appdata_hex(&doc)?;
107 Ok(format!("0x{}", alloy_primitives::hex::encode(hash.as_slice())))
108}
109
110/// Build the `0x`-prefixed app-data hex string with the given `app_code` and
111/// [`Metadata`].
112///
113/// This is the full-featured variant of [`build_order_app_data`]: it lets
114/// callers embed structured metadata (order class, quote slippage, UTM params,
115/// hooks, partner fees, …) in the app-data document before hashing.
116///
117/// Internally delegates to [`build_app_data_doc_full`] and discards the full
118/// JSON string, returning only the `keccak256` hex digest. If you also need
119/// the canonical JSON (e.g. to upload to IPFS), call [`build_app_data_doc_full`]
120/// directly.
121///
122/// Mirrors `buildAppData` from the `@cowprotocol/app-data` `TypeScript` package.
123///
124/// # Parameters
125///
126/// * `app_code` — application identifier (e.g. `"CoW Swap"`).
127/// * `metadata` — structured metadata to embed. Use [`Metadata::default()`] for an empty metadata
128/// block, or the builder methods on [`Metadata`] to populate individual fields.
129///
130/// # Returns
131///
132/// A `0x`-prefixed, lowercase hex string of the 32-byte `keccak256` hash.
133///
134/// # Errors
135///
136/// Returns [`CowError::AppData`] if serialisation fails.
137///
138/// # Example
139///
140/// ```
141/// use cow_rs::app_data::{Metadata, Quote, build_app_data_doc};
142///
143/// let meta = Metadata::default().with_quote(Quote::new(50));
144/// let hex = build_app_data_doc("MyDApp", meta).unwrap();
145/// assert!(hex.starts_with("0x"));
146/// ```
147pub fn build_app_data_doc(app_code: &str, metadata: Metadata) -> Result<String, CowError> {
148 let (_, hash_hex) = build_app_data_doc_full(app_code, metadata)?;
149 Ok(hash_hex)
150}
151
152/// Build the canonical JSON app-data document together with its `keccak256`
153/// hash.
154///
155/// This is the lowest-level entry point for app-data construction. It
156/// assembles an [`AppDataDoc`] from the given `app_code` and [`Metadata`],
157/// serialises it to deterministic JSON (sorted keys, no whitespace), and
158/// returns both the full JSON string and the `0x`-prefixed `keccak256` hex
159/// digest.
160///
161/// Use this when you need both the canonical JSON (e.g. to pin on IPFS) and
162/// the hash (to submit in the on-chain order). If you only need the hash,
163/// prefer [`build_app_data_doc`]; for simple orders without metadata, use
164/// [`build_order_app_data`].
165///
166/// Mirrors the `TypeScript` SDK's `buildAppData` which returns a
167/// `TradingAppDataInfo` object with `fullAppData` and `appDataKeccak256`.
168///
169/// # Parameters
170///
171/// * `app_code` — application identifier (e.g. `"CoW Swap"`).
172/// * `metadata` — structured metadata to embed in the document.
173///
174/// # Returns
175///
176/// A tuple `(full_app_data_json, app_data_keccak256_hex)` where:
177/// - `full_app_data_json` is the canonical JSON string with sorted keys.
178/// - `app_data_keccak256_hex` is the `0x`-prefixed 32-byte hex digest.
179///
180/// # Errors
181///
182/// Returns [`CowError::AppData`] if the document cannot be serialised to JSON.
183///
184/// # Example
185///
186/// ```
187/// use cow_rs::app_data::{Metadata, build_app_data_doc_full};
188///
189/// let (json, hex) = build_app_data_doc_full("MyDApp", Metadata::default()).unwrap();
190/// assert!(json.contains("MyDApp"));
191/// assert!(hex.starts_with("0x"));
192/// assert_eq!(hex.len(), 66);
193/// ```
194// The return type is a transparent (json, hash_hex) pair — the tuple is intentional.
195#[allow(
196 clippy::type_complexity,
197 reason = "transparent (json, hash_hex) pair; no named type needed"
198)]
199pub fn build_app_data_doc_full(
200 app_code: &str,
201 metadata: Metadata,
202) -> Result<(String, String), CowError> {
203 let doc = AppDataDoc {
204 version: LATEST_APP_DATA_VERSION.to_owned(),
205 app_code: Some(app_code.to_owned()),
206 environment: None,
207 metadata,
208 };
209 let json = stringify_deterministic(&doc)?;
210 let hash = alloy_primitives::keccak256(json.as_bytes());
211 let hash_hex = format!("0x{}", alloy_primitives::hex::encode(hash.as_slice()));
212 Ok((json, hash_hex))
213}
214
215/// Return the canonical JSON string for `doc` with all object keys sorted
216/// alphabetically and no extraneous whitespace.
217///
218/// This is a thin convenience wrapper around [`stringify_deterministic`]. Use
219/// it when you need the raw JSON pre-image that, when hashed with
220/// `keccak256`, yields the `appData` value stored on-chain.
221///
222/// # Parameters
223///
224/// * `doc` — the [`AppDataDoc`] to serialise.
225///
226/// # Returns
227///
228/// A compact JSON string with deterministically ordered keys.
229///
230/// # Errors
231///
232/// Returns [`CowError::AppData`] if serialisation fails.
233///
234/// # Example
235///
236/// ```
237/// use cow_rs::app_data::{AppDataDoc, appdata_json};
238///
239/// let doc = AppDataDoc::new("MyDApp");
240/// let json = appdata_json(&doc).unwrap();
241/// // Keys are sorted: "appCode" comes before "metadata" comes before "version"
242/// assert!(json.starts_with('{'));
243/// assert!(json.contains("\"appCode\":\"MyDApp\""));
244/// ```
245pub fn appdata_json(doc: &AppDataDoc) -> Result<String, CowError> {
246 stringify_deterministic(doc)
247}
248
249/// Serialise `doc` to a deterministic JSON string with all object keys
250/// sorted alphabetically at every nesting level.
251///
252/// This is the core serialisation primitive that underpins all app-data
253/// hashing in this crate. It guarantees that two [`AppDataDoc`] values with
254/// identical logical content always produce byte-identical JSON, regardless
255/// of Rust struct field order or `serde` attribute ordering.
256///
257/// Matches the behaviour of `json-stringify-deterministic` used by the
258/// `TypeScript` SDK, ensuring cross-language hash compatibility.
259///
260/// # Parameters
261///
262/// * `doc` — the [`AppDataDoc`] to serialise.
263///
264/// # Returns
265///
266/// A compact JSON string with no whitespace between tokens and all object
267/// keys recursively sorted in lexicographic order.
268///
269/// # Errors
270///
271/// Returns [`CowError::AppData`] on serialisation failure.
272///
273/// # Example
274///
275/// ```
276/// use cow_rs::app_data::{AppDataDoc, stringify_deterministic};
277///
278/// let doc = AppDataDoc::new("Test");
279/// let json = stringify_deterministic(&doc).unwrap();
280/// // Deterministic: calling twice yields the exact same bytes.
281/// assert_eq!(json, stringify_deterministic(&doc).unwrap());
282/// ```
283pub fn stringify_deterministic(doc: &AppDataDoc) -> Result<String, CowError> {
284 let value = serde_json::to_value(doc).map_err(|e| CowError::AppData(e.to_string()))?;
285 let sorted = sort_keys(value);
286 serde_json::to_string(&sorted).map_err(|e| CowError::AppData(e.to_string()))
287}
288
289/// Deep-merge `other` into `base` and return the result.
290///
291/// Scalar fields (`version`, `app_code`, `environment`) use `other`'s value
292/// when it is non-empty / `Some`. Each [`Metadata`] field is replaced
293/// independently when the corresponding field in `other.metadata` is `Some`.
294///
295/// Array-typed fields inside `OrderInteractionHooks` (`pre` / `post` hooks)
296/// are **replaced wholesale** when `other.metadata.hooks` is `Some` — this
297/// matches the `TypeScript` SDK's array-clearing deep-merge semantics.
298///
299/// Mirrors `mergeAppDataDoc` from the `@cowprotocol/app-data` package.
300///
301/// # Example
302///
303/// ```
304/// use cow_rs::app_data::{AppDataDoc, merge_app_data_doc};
305///
306/// let base = AppDataDoc::new("BaseApp");
307/// let override_doc = AppDataDoc::new("OverrideApp");
308/// let merged = merge_app_data_doc(base, override_doc);
309/// assert_eq!(merged.app_code, Some("OverrideApp".to_owned()));
310/// ```
311#[must_use]
312pub fn merge_app_data_doc(mut base: AppDataDoc, other: AppDataDoc) -> AppDataDoc {
313 if !other.version.is_empty() {
314 base.version = other.version;
315 }
316 if other.app_code.is_some() {
317 base.app_code = other.app_code;
318 }
319 if other.environment.is_some() {
320 base.environment = other.environment;
321 }
322 let om = other.metadata;
323 if om.referrer.is_some() {
324 base.metadata.referrer = om.referrer;
325 }
326 if om.utm.is_some() {
327 base.metadata.utm = om.utm;
328 }
329 if om.quote.is_some() {
330 base.metadata.quote = om.quote;
331 }
332 if om.order_class.is_some() {
333 base.metadata.order_class = om.order_class;
334 }
335 if om.hooks.is_some() {
336 base.metadata.hooks = om.hooks;
337 }
338 if om.widget.is_some() {
339 base.metadata.widget = om.widget;
340 }
341 if om.partner_fee.is_some() {
342 base.metadata.partner_fee = om.partner_fee;
343 }
344 if om.replaced_order.is_some() {
345 base.metadata.replaced_order = om.replaced_order;
346 }
347 if om.signer.is_some() {
348 base.metadata.signer = om.signer;
349 }
350 base
351}
352
353/// Recursively sort all object keys in a [`Value`] alphabetically.
354fn sort_keys(value: Value) -> Value {
355 match value {
356 Value::Object(map) => {
357 let mut pairs: Vec<(String, Value)> =
358 map.into_iter().map(|(k, v)| (k, sort_keys(v))).collect();
359 pairs.sort_by(|a, b| a.0.cmp(&b.0));
360 Value::Object(pairs.into_iter().collect())
361 }
362 Value::Array(arr) => Value::Array(arr.into_iter().map(sort_keys).collect()),
363 other @ (Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)) => other,
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn appdata_hex_is_deterministic() {
373 let doc = AppDataDoc::new("Test");
374 let h1 = appdata_hex(&doc).unwrap();
375 let h2 = appdata_hex(&doc).unwrap();
376 assert_eq!(h1, h2);
377 assert_ne!(h1, B256::ZERO);
378 }
379
380 #[test]
381 fn build_order_app_data_format() {
382 let hex = build_order_app_data("MyDApp").unwrap();
383 assert!(hex.starts_with("0x"));
384 assert_eq!(hex.len(), 66);
385 }
386
387 #[test]
388 fn build_app_data_doc_returns_hex() {
389 let hex = build_app_data_doc("MyDApp", Metadata::default()).unwrap();
390 assert!(hex.starts_with("0x"));
391 assert_eq!(hex.len(), 66);
392 }
393
394 #[test]
395 fn build_app_data_doc_full_returns_json_and_hex() {
396 let (json, hex) = build_app_data_doc_full("MyDApp", Metadata::default()).unwrap();
397 assert!(json.contains("MyDApp"));
398 assert!(hex.starts_with("0x"));
399 assert_eq!(hex.len(), 66);
400 }
401
402 #[test]
403 fn appdata_json_deterministic() {
404 let doc = AppDataDoc::new("Test");
405 let j1 = appdata_json(&doc).unwrap();
406 let j2 = appdata_json(&doc).unwrap();
407 assert_eq!(j1, j2);
408 assert!(j1.starts_with('{'));
409 }
410
411 #[test]
412 fn stringify_deterministic_sorts_keys() {
413 let doc = AppDataDoc::new("Test");
414 let json = stringify_deterministic(&doc).unwrap();
415 // Keys should be sorted: appCode before metadata before version
416 let app_idx = json.find("appCode").unwrap();
417 let meta_idx = json.find("metadata").unwrap();
418 let ver_idx = json.find("version").unwrap();
419 assert!(app_idx < meta_idx);
420 assert!(meta_idx < ver_idx);
421 }
422
423 #[test]
424 fn merge_app_data_doc_overrides_app_code() {
425 let base = AppDataDoc::new("Base");
426 let other = AppDataDoc::new("Override");
427 let merged = merge_app_data_doc(base, other);
428 assert_eq!(merged.app_code, Some("Override".to_owned()));
429 }
430
431 #[test]
432 fn merge_app_data_doc_overrides_version() {
433 let base = AppDataDoc::new("Base");
434 let mut other = AppDataDoc::new("Other");
435 other.version = "2.0.0".to_owned();
436 let merged = merge_app_data_doc(base, other);
437 assert_eq!(merged.version, "2.0.0");
438 }
439
440 #[test]
441 fn merge_app_data_doc_preserves_base_when_other_empty() {
442 let base = AppDataDoc::new("Base").with_environment("prod");
443 let other = AppDataDoc {
444 version: String::new(),
445 app_code: None,
446 environment: None,
447 metadata: Metadata::default(),
448 };
449 let merged = merge_app_data_doc(base, other);
450 // Empty version on other means base version is kept
451 assert_eq!(merged.app_code, Some("Base".to_owned()));
452 assert_eq!(merged.environment, Some("prod".to_owned()));
453 }
454
455 #[test]
456 fn merge_app_data_doc_overrides_environment() {
457 let base = AppDataDoc::new("Base");
458 let other = AppDataDoc::new("Other").with_environment("staging");
459 let merged = merge_app_data_doc(base, other);
460 assert_eq!(merged.environment.as_deref(), Some("staging"));
461 }
462
463 #[test]
464 fn merge_app_data_doc_overrides_metadata_fields() {
465 use crate::app_data::types::{Quote, Referrer, Utm, Widget};
466 let base = AppDataDoc::new("Base");
467 let other = AppDataDoc {
468 version: LATEST_APP_DATA_VERSION.to_owned(),
469 app_code: None,
470 environment: None,
471 metadata: Metadata::default()
472 .with_referrer(Referrer::code("ABC"))
473 .with_utm(Utm { utm_source: Some("test".into()), ..Default::default() })
474 .with_quote(Quote::new(50))
475 .with_widget(Widget { app_code: "w".into(), environment: None })
476 .with_signer("0x1111111111111111111111111111111111111111"),
477 };
478 let merged = merge_app_data_doc(base, other);
479 assert!(merged.metadata.referrer.is_some());
480 assert!(merged.metadata.utm.is_some());
481 assert!(merged.metadata.quote.is_some());
482 assert!(merged.metadata.widget.is_some());
483 assert!(merged.metadata.signer.is_some());
484 }
485
486 #[test]
487 fn merge_app_data_doc_overrides_hooks() {
488 use crate::app_data::types::{CowHook, OrderInteractionHooks};
489 let base = AppDataDoc::new("Base");
490 let hook =
491 CowHook::new("0x0000000000000000000000000000000000000001", "0xdeadbeef", "100000");
492 let other = AppDataDoc {
493 version: LATEST_APP_DATA_VERSION.to_owned(),
494 app_code: None,
495 environment: None,
496 metadata: Metadata::default()
497 .with_hooks(OrderInteractionHooks::new(vec![hook], vec![])),
498 };
499 let merged = merge_app_data_doc(base, other);
500 assert!(merged.metadata.hooks.is_some());
501 }
502
503 #[test]
504 fn merge_app_data_doc_overrides_order_class() {
505 use crate::app_data::types::OrderClassKind;
506 let base = AppDataDoc::new("Base");
507 let other = AppDataDoc::new("Other").with_order_class(OrderClassKind::Twap);
508 let merged = merge_app_data_doc(base, other);
509 assert!(merged.metadata.order_class.is_some());
510 }
511
512 #[test]
513 fn merge_app_data_doc_overrides_partner_fee() {
514 use crate::app_data::types::{PartnerFee, PartnerFeeEntry};
515 let base = AppDataDoc::new("Base");
516 let other = AppDataDoc {
517 version: LATEST_APP_DATA_VERSION.to_owned(),
518 app_code: None,
519 environment: None,
520 metadata: Metadata::default().with_partner_fee(PartnerFee::Single(
521 PartnerFeeEntry::volume(50, "0x0000000000000000000000000000000000000001"),
522 )),
523 };
524 let merged = merge_app_data_doc(base, other);
525 assert!(merged.metadata.partner_fee.is_some());
526 }
527
528 #[test]
529 fn merge_app_data_doc_overrides_replaced_order() {
530 let base = AppDataDoc::new("Base");
531 let uid = format!("0x{}", "ab".repeat(56));
532 let other = AppDataDoc::new("Other").with_replaced_order(uid);
533 let merged = merge_app_data_doc(base, other);
534 assert!(merged.metadata.replaced_order.is_some());
535 }
536
537 #[test]
538 fn sort_keys_handles_arrays_and_nested() {
539 let v = serde_json::json!({
540 "b": [{"z": 1, "a": 2}],
541 "a": null,
542 });
543 let sorted = sort_keys(v);
544 let s = serde_json::to_string(&sorted).unwrap();
545 // "a" should come before "b" in the output
546 let a_idx = s.find("\"a\"").unwrap();
547 let b_idx = s.find("\"b\"").unwrap();
548 assert!(a_idx < b_idx);
549 // Inside the array, "a" should come before "z"
550 let inner_a = s.rfind("\"a\"").unwrap();
551 let inner_z = s.find("\"z\"").unwrap();
552 assert!(inner_a < inner_z);
553 }
554}