use alloy_primitives::{B256, keccak256};
use serde_json::Value;
use crate::error::CowError;
use super::types::{AppDataDoc, LATEST_APP_DATA_VERSION, Metadata};
pub fn appdata_hex(doc: &AppDataDoc) -> Result<B256, CowError> {
let json = stringify_deterministic(doc)?;
Ok(keccak256(json.as_bytes()))
}
pub fn build_order_app_data(app_code: &str) -> Result<String, CowError> {
let doc = AppDataDoc::new(app_code);
let hash = appdata_hex(&doc)?;
Ok(format!("0x{}", alloy_primitives::hex::encode(hash.as_slice())))
}
pub fn build_app_data_doc(app_code: &str, metadata: Metadata) -> Result<String, CowError> {
let (_, hash_hex) = build_app_data_doc_full(app_code, metadata)?;
Ok(hash_hex)
}
#[allow(
clippy::type_complexity,
reason = "transparent (json, hash_hex) pair; no named type needed"
)]
pub fn build_app_data_doc_full(
app_code: &str,
metadata: Metadata,
) -> Result<(String, String), CowError> {
let doc = AppDataDoc {
version: LATEST_APP_DATA_VERSION.to_owned(),
app_code: Some(app_code.to_owned()),
environment: None,
metadata,
};
let json = stringify_deterministic(&doc)?;
let hash = alloy_primitives::keccak256(json.as_bytes());
let hash_hex = format!("0x{}", alloy_primitives::hex::encode(hash.as_slice()));
Ok((json, hash_hex))
}
pub fn appdata_json(doc: &AppDataDoc) -> Result<String, CowError> {
stringify_deterministic(doc)
}
pub fn stringify_deterministic(doc: &AppDataDoc) -> Result<String, CowError> {
let value = serde_json::to_value(doc).map_err(|e| CowError::AppData(e.to_string()))?;
let sorted = sort_keys(value);
serde_json::to_string(&sorted).map_err(|e| CowError::AppData(e.to_string()))
}
#[must_use]
pub fn merge_app_data_doc(mut base: AppDataDoc, other: AppDataDoc) -> AppDataDoc {
if !other.version.is_empty() {
base.version = other.version;
}
if other.app_code.is_some() {
base.app_code = other.app_code;
}
if other.environment.is_some() {
base.environment = other.environment;
}
let om = other.metadata;
if om.referrer.is_some() {
base.metadata.referrer = om.referrer;
}
if om.utm.is_some() {
base.metadata.utm = om.utm;
}
if om.quote.is_some() {
base.metadata.quote = om.quote;
}
if om.order_class.is_some() {
base.metadata.order_class = om.order_class;
}
if om.hooks.is_some() {
base.metadata.hooks = om.hooks;
}
if om.widget.is_some() {
base.metadata.widget = om.widget;
}
if om.partner_fee.is_some() {
base.metadata.partner_fee = om.partner_fee;
}
if om.replaced_order.is_some() {
base.metadata.replaced_order = om.replaced_order;
}
if om.signer.is_some() {
base.metadata.signer = om.signer;
}
base
}
fn sort_keys(value: Value) -> Value {
match value {
Value::Object(map) => {
let mut pairs: Vec<(String, Value)> =
map.into_iter().map(|(k, v)| (k, sort_keys(v))).collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
Value::Object(pairs.into_iter().collect())
}
Value::Array(arr) => Value::Array(arr.into_iter().map(sort_keys).collect()),
other @ (Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)) => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn appdata_hex_is_deterministic() {
let doc = AppDataDoc::new("Test");
let h1 = appdata_hex(&doc).unwrap();
let h2 = appdata_hex(&doc).unwrap();
assert_eq!(h1, h2);
assert_ne!(h1, B256::ZERO);
}
#[test]
fn build_order_app_data_format() {
let hex = build_order_app_data("MyDApp").unwrap();
assert!(hex.starts_with("0x"));
assert_eq!(hex.len(), 66);
}
#[test]
fn build_app_data_doc_returns_hex() {
let hex = build_app_data_doc("MyDApp", Metadata::default()).unwrap();
assert!(hex.starts_with("0x"));
assert_eq!(hex.len(), 66);
}
#[test]
fn build_app_data_doc_full_returns_json_and_hex() {
let (json, hex) = build_app_data_doc_full("MyDApp", Metadata::default()).unwrap();
assert!(json.contains("MyDApp"));
assert!(hex.starts_with("0x"));
assert_eq!(hex.len(), 66);
}
#[test]
fn appdata_json_deterministic() {
let doc = AppDataDoc::new("Test");
let j1 = appdata_json(&doc).unwrap();
let j2 = appdata_json(&doc).unwrap();
assert_eq!(j1, j2);
assert!(j1.starts_with('{'));
}
#[test]
fn stringify_deterministic_sorts_keys() {
let doc = AppDataDoc::new("Test");
let json = stringify_deterministic(&doc).unwrap();
let app_idx = json.find("appCode").unwrap();
let meta_idx = json.find("metadata").unwrap();
let ver_idx = json.find("version").unwrap();
assert!(app_idx < meta_idx);
assert!(meta_idx < ver_idx);
}
#[test]
fn merge_app_data_doc_overrides_app_code() {
let base = AppDataDoc::new("Base");
let other = AppDataDoc::new("Override");
let merged = merge_app_data_doc(base, other);
assert_eq!(merged.app_code, Some("Override".to_owned()));
}
#[test]
fn merge_app_data_doc_overrides_version() {
let base = AppDataDoc::new("Base");
let mut other = AppDataDoc::new("Other");
other.version = "2.0.0".to_owned();
let merged = merge_app_data_doc(base, other);
assert_eq!(merged.version, "2.0.0");
}
#[test]
fn merge_app_data_doc_preserves_base_when_other_empty() {
let base = AppDataDoc::new("Base").with_environment("prod");
let other = AppDataDoc {
version: String::new(),
app_code: None,
environment: None,
metadata: Metadata::default(),
};
let merged = merge_app_data_doc(base, other);
assert_eq!(merged.app_code, Some("Base".to_owned()));
assert_eq!(merged.environment, Some("prod".to_owned()));
}
#[test]
fn merge_app_data_doc_overrides_environment() {
let base = AppDataDoc::new("Base");
let other = AppDataDoc::new("Other").with_environment("staging");
let merged = merge_app_data_doc(base, other);
assert_eq!(merged.environment.as_deref(), Some("staging"));
}
#[test]
fn merge_app_data_doc_overrides_metadata_fields() {
use crate::app_data::types::{Quote, Referrer, Utm, Widget};
let base = AppDataDoc::new("Base");
let other = AppDataDoc {
version: LATEST_APP_DATA_VERSION.to_owned(),
app_code: None,
environment: None,
metadata: Metadata::default()
.with_referrer(Referrer::code("ABC"))
.with_utm(Utm { utm_source: Some("test".into()), ..Default::default() })
.with_quote(Quote::new(50))
.with_widget(Widget { app_code: "w".into(), environment: None })
.with_signer("0x1111111111111111111111111111111111111111"),
};
let merged = merge_app_data_doc(base, other);
assert!(merged.metadata.referrer.is_some());
assert!(merged.metadata.utm.is_some());
assert!(merged.metadata.quote.is_some());
assert!(merged.metadata.widget.is_some());
assert!(merged.metadata.signer.is_some());
}
#[test]
fn merge_app_data_doc_overrides_hooks() {
use crate::app_data::types::{CowHook, OrderInteractionHooks};
let base = AppDataDoc::new("Base");
let hook =
CowHook::new("0x0000000000000000000000000000000000000001", "0xdeadbeef", "100000");
let other = AppDataDoc {
version: LATEST_APP_DATA_VERSION.to_owned(),
app_code: None,
environment: None,
metadata: Metadata::default()
.with_hooks(OrderInteractionHooks::new(vec![hook], vec![])),
};
let merged = merge_app_data_doc(base, other);
assert!(merged.metadata.hooks.is_some());
}
#[test]
fn merge_app_data_doc_overrides_order_class() {
use crate::app_data::types::OrderClassKind;
let base = AppDataDoc::new("Base");
let other = AppDataDoc::new("Other").with_order_class(OrderClassKind::Twap);
let merged = merge_app_data_doc(base, other);
assert!(merged.metadata.order_class.is_some());
}
#[test]
fn merge_app_data_doc_overrides_partner_fee() {
use crate::app_data::types::{PartnerFee, PartnerFeeEntry};
let base = AppDataDoc::new("Base");
let other = AppDataDoc {
version: LATEST_APP_DATA_VERSION.to_owned(),
app_code: None,
environment: None,
metadata: Metadata::default().with_partner_fee(PartnerFee::Single(
PartnerFeeEntry::volume(50, "0x0000000000000000000000000000000000000001"),
)),
};
let merged = merge_app_data_doc(base, other);
assert!(merged.metadata.partner_fee.is_some());
}
#[test]
fn merge_app_data_doc_overrides_replaced_order() {
let base = AppDataDoc::new("Base");
let uid = format!("0x{}", "ab".repeat(56));
let other = AppDataDoc::new("Other").with_replaced_order(uid);
let merged = merge_app_data_doc(base, other);
assert!(merged.metadata.replaced_order.is_some());
}
#[test]
fn sort_keys_handles_arrays_and_nested() {
let v = serde_json::json!({
"b": [{"z": 1, "a": 2}],
"a": null,
});
let sorted = sort_keys(v);
let s = serde_json::to_string(&sorted).unwrap();
let a_idx = s.find("\"a\"").unwrap();
let b_idx = s.find("\"b\"").unwrap();
assert!(a_idx < b_idx);
let inner_a = s.rfind("\"a\"").unwrap();
let inner_z = s.find("\"z\"").unwrap();
assert!(inner_a < inner_z);
}
}