Skip to main content

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}