use {
super::*,
::cid::multihash::Multihash,
alloy_primitives::{address, b256},
};
#[test]
fn json_round_trip_zero() {
let zero = AppDataHash::default();
let json = serde_json::to_value(zero).unwrap();
assert_eq!(
json,
serde_json::json!("0x0000000000000000000000000000000000000000000000000000000000000000")
);
let parsed: AppDataHash = serde_json::from_value(json).unwrap();
assert_eq!(parsed, zero);
}
#[test]
fn json_round_trip_non_zero() {
let mut bytes = [0u8; 32];
bytes[0] = 0xab;
bytes[31] = 0xcd;
let original = AppDataHash::from(bytes);
let json = serde_json::to_value(original).unwrap();
let parsed: AppDataHash = serde_json::from_value(json).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn rejects_wrong_length() {
let json = serde_json::json!("0xabcd");
let result: Result<AppDataHash, _> = serde_json::from_value(json);
assert!(result.is_err());
}
#[test]
fn empty_app_data_hash_matches_keccak() {
let computed = alloy_primitives::keccak256(EMPTY_APP_DATA_JSON);
assert_eq!(EMPTY_APP_DATA_HASH, computed);
}
#[test]
fn sdk_attribution_doc_pins_app_code_and_version() {
for app_code in [COW_RS_APP_CODE, COW_RS_WASM_APP_CODE] {
let doc = AppDataDoc::sdk_attribution(app_code);
assert_eq!(doc.app_code.as_deref(), Some(app_code));
let json = doc.canonical_json();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["appCode"], app_code);
assert_eq!(
value["metadata"]["quote"]["version"],
env!("CARGO_PKG_VERSION")
);
assert_eq!(value["version"], LATEST_APP_DATA_VERSION);
assert_eq!(
AppDataDoc::sdk_attribution(app_code).hash(),
AppDataDoc::sdk_attribution(app_code).hash(),
);
}
assert_ne!(
AppDataDoc::sdk_attribution(COW_RS_APP_CODE).hash(),
AppDataDoc::sdk_attribution(COW_RS_WASM_APP_CODE).hash(),
"cow-rs and cow-rs-wasm must produce distinct app-data digests"
);
}
#[test]
fn empty_doc_matches_constant() {
let doc = AppDataDoc::new("");
assert!(doc.metadata.quote.is_none());
assert!(doc.metadata.order_class.is_none());
assert!(doc.metadata.partner_fee.is_none());
assert!(doc.metadata.referrer.is_none());
assert!(doc.metadata.utm.is_none());
assert!(doc.metadata.hooks.is_none());
assert!(doc.metadata.flashloan.is_none());
assert!(doc.metadata.replaced_order.is_none());
assert!(doc.metadata.wrappers.is_empty());
let json = doc.canonical_json();
assert_eq!(json, r#"{"appCode":"","metadata":{},"version":"1.6.0"}"#);
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, doc);
}
#[test]
fn referrer_doc_round_trips() {
let referrer = address!("1234567890AbcdEF1234567890aBcdef12345678");
let doc = AppDataDoc::new("my-app").with_referrer(referrer);
let json = doc.canonical_json();
assert!(
json.to_lowercase()
.contains(r#""referrer":{"address":"0x1234567890abcdef1234567890abcdef12345678"}"#)
);
let hash = doc.hash();
let direct = alloy_primitives::keccak256(json.as_bytes());
assert_eq!(hash, direct);
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
let parsed_referrer = parsed.metadata.referrer.expect("referrer preserved");
assert_eq!(parsed_referrer.address, referrer);
}
#[test]
fn minimal_doc_golden_hash() {
let doc = AppDataDoc::new("");
assert_eq!(
doc.canonical_json(),
r#"{"appCode":"","metadata":{},"version":"1.6.0"}"#
);
let expected = b256!("3929e2c230dc41c0c053ff5f9211eb32def3a737b2bf36eb5b8862ea317fcd9e");
assert_eq!(doc.hash(), expected);
}
#[test]
fn canonical_json_preserves_utf8_non_ascii_bytes() {
let doc = AppDataDoc::new("café-\u{1F40c}"); let json = doc.canonical_json();
assert!(
json.contains("café-\u{1F40c}"),
"expected raw UTF-8 non-ASCII bytes in canonical JSON, got: {json}"
);
assert!(
!json.contains("\\u00e9"),
"expected raw UTF-8, found ASCII escape: {json}"
);
assert!(
!json.contains("\\ud83d"),
"expected raw UTF-8, found surrogate-pair escape: {json}"
);
let direct = alloy_primitives::keccak256(json.as_bytes());
assert_eq!(doc.hash(), direct);
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.app_code.as_deref(), Some("café-\u{1F40c}"));
}
#[test]
fn canonical_json_sorts_keys_deterministically() {
let doc = AppDataDoc::new("app")
.with_referrer(address!("0000000000000000000000000000000000000001"))
.with_partner_fee(50, address!("0000000000000000000000000000000000000002"))
.unwrap()
.with_order_class(OrderClass::Limit)
.with_slippage_bips(25)
.with_environment("prod");
let json = doc.canonical_json();
let app_code_pos = json.find("appCode").unwrap();
let environment_pos = json.find("environment").unwrap();
let metadata_pos = json.find("metadata").unwrap();
let version_pos = json.find("\"version\"").unwrap();
assert!(app_code_pos < environment_pos);
assert!(environment_pos < metadata_pos);
assert!(metadata_pos < version_pos);
let order_class_pos = json.find("orderClass").unwrap();
let partner_fee_pos = json.find("partnerFee").unwrap();
let quote_pos = json.find("\"quote\"").unwrap();
let referrer_pos = json.find("referrer").unwrap();
assert!(order_class_pos < partner_fee_pos);
assert!(partner_fee_pos < quote_pos);
assert!(quote_pos < referrer_pos);
assert_eq!(json, doc.canonical_json());
}
#[test]
fn partner_fee_volume_round_trips_with_legacy_bps_key() {
let recipient = address!("00000000219AB540356CBb839CbE05303D7705FA");
let doc = AppDataDoc::new("app")
.with_partner_fee(75, recipient)
.unwrap();
let json = doc.canonical_json();
assert!(
json.to_lowercase()
.contains(r#""partnerfee":{"bps":75,"recipient":"#),
"got: {json}",
);
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
let fee = parsed.metadata.partner_fee.expect("partner fee preserved");
assert!(matches!(fee.policy(), FeePolicy::Volume { bps: 75 }));
assert_eq!(fee.recipient(), recipient);
}
#[test]
fn partner_fee_surplus_emits_typed_keys() {
let recipient = address!("00000000219AB540356CBb839CbE05303D7705FA");
let doc = AppDataDoc::new("app")
.with_partner_fee_policy(
FeePolicy::Surplus {
bps: 25,
max_volume_bps: 100,
},
recipient,
)
.unwrap();
let json = doc.canonical_json();
assert!(json.contains(r#""maxVolumeBps":100"#), "got: {json}");
assert!(json.contains(r#""surplusBps":25"#), "got: {json}");
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
let fee = parsed.metadata.partner_fee.expect("partner fee preserved");
assert!(matches!(
fee.policy(),
FeePolicy::Surplus {
bps: 25,
max_volume_bps: 100,
}
));
}
#[test]
fn partner_fee_price_improvement_emits_typed_keys() {
let recipient = address!("00000000219AB540356CBb839CbE05303D7705FA");
let doc = AppDataDoc::new("app")
.with_partner_fee_policy(
FeePolicy::PriceImprovement {
bps: 30,
max_volume_bps: 150,
},
recipient,
)
.unwrap();
let json = doc.canonical_json();
assert!(json.contains(r#""priceImprovementBps":30"#), "got: {json}");
assert!(json.contains(r#""maxVolumeBps":150"#), "got: {json}");
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
let fee = parsed.metadata.partner_fee.expect("partner fee preserved");
assert!(matches!(
fee.policy(),
FeePolicy::PriceImprovement {
bps: 30,
max_volume_bps: 150,
}
));
}
#[test]
fn partner_fee_deserialises_volume_bps_alias() {
let recipient = address!("00000000219AB540356CBb839CbE05303D7705FA");
let json = format!(r#"{{"volumeBps":42,"recipient":"{recipient:?}"}}"#,);
let fee: AppDataPartnerFee = serde_json::from_str(&json).unwrap();
assert!(matches!(fee.policy(), FeePolicy::Volume { bps: 42 }));
assert_eq!(fee.recipient(), recipient);
}
#[test]
fn partner_fee_builder_rejects_over_cap_bps() {
let recipient = address!("00000000219AB540356CBb839CbE05303D7705FA");
let err = AppDataDoc::new("app")
.with_partner_fee(10_001, recipient)
.unwrap_err();
assert!(matches!(
err,
AppDataError::FeeOutOfRange {
field: "bps",
value: 10_001,
max: PARTNER_FEE_BPS_MAX,
}
));
let err = AppDataDoc::new("app")
.with_partner_fee_policy(
FeePolicy::Surplus {
bps: 1,
max_volume_bps: 10_001,
},
recipient,
)
.unwrap_err();
assert!(matches!(
err,
AppDataError::FeeOutOfRange {
field: "maxVolumeBps",
value: 10_001,
..
}
));
let err = AppDataDoc::new("app")
.with_partner_fee_policy(
FeePolicy::PriceImprovement {
bps: 10_001,
max_volume_bps: 1,
},
recipient,
)
.unwrap_err();
assert!(matches!(
err,
AppDataError::FeeOutOfRange {
field: "priceImprovementBps",
value: 10_001,
..
}
));
let _ = AppDataDoc::new("app")
.with_partner_fee(PARTNER_FEE_BPS_MAX, recipient)
.expect("bps at the cap must be accepted");
}
#[test]
fn partner_fee_rejects_mixed_policy_fields() {
let recipient = address!("00000000219AB540356CBb839CbE05303D7705FA");
let json = format!(
r#"{{"surplusBps":10,"priceImprovementBps":20,"maxVolumeBps":50,"recipient":"{recipient:?}"}}"#,
);
let err = serde_json::from_str::<AppDataPartnerFee>(&json).unwrap_err();
assert!(err.to_string().contains("unknown partner-fee policy"));
}
#[test]
fn flashloan_round_trips() {
let flashloan = AppDataFlashloan {
liquidity_provider: address!("1111111111111111111111111111111111111111"),
protocol_adapter: address!("2222222222222222222222222222222222222222"),
receiver: address!("3333333333333333333333333333333333333333"),
token: address!("4444444444444444444444444444444444444444"),
amount: U256::from(1_000_000_u64),
};
let doc = AppDataDoc::new("app").with_flashloan(flashloan.clone());
let json = doc.canonical_json();
assert!(
json.contains(r#""amount":"1000000""#),
"amount must serialise as decimal string, got: {json}",
);
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.metadata.flashloan.expect("flashloan preserved"),
flashloan
);
}
#[test]
fn replaced_order_round_trips() {
let uid = OrderUid::from([0x55; 56]);
let doc = AppDataDoc::new("app").with_replaced_order(uid);
let json = doc.canonical_json();
assert!(
json.contains(r#""replacedOrder":{"uid":"0x"#),
"got: {json}"
);
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.metadata.replaced_order.expect("replaced order").uid,
uid
);
}
#[test]
fn wrappers_round_trip_and_skip_when_empty() {
let doc = AppDataDoc::new("app");
assert!(!doc.canonical_json().contains("wrappers"));
let wrapper = AppDataWrapperCall {
address: address!("5555555555555555555555555555555555555555"),
data: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
is_omittable: true,
};
let doc = doc.with_wrapper(wrapper.clone());
let json = doc.canonical_json();
assert!(json.contains(r#""data":"0xdeadbeef""#), "got: {json}");
assert!(json.contains(r#""isOmittable":true"#), "got: {json}");
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.metadata.wrappers, vec![wrapper]);
}
#[test]
fn order_class_serialises_as_lowercase() {
let doc = AppDataDoc::new("app").with_order_class(OrderClass::Market);
let json = doc.canonical_json();
assert!(json.contains(r#""orderClass":{"orderClass":"market"}"#));
}
#[test]
fn hooks_pass_through_as_opaque_json() {
let mut doc = AppDataDoc::new("app");
doc.metadata.hooks = Some(serde_json::json!({
"version": "0.1.0",
"pre": [{"target": "0xabc", "callData": "0xdef", "gasLimit": "21000"}],
"post": [],
}));
let json = doc.canonical_json();
let parsed: AppDataDoc = serde_json::from_str(&json).unwrap();
let hooks = parsed.metadata.hooks.expect("hooks preserved");
assert_eq!(hooks["version"], "0.1.0");
assert_eq!(hooks["pre"][0]["target"], "0xabc");
}
#[test]
fn cid_round_trip_default_and_walking_bytes() {
let default = AppDataHash::default();
assert_eq!(
app_data_hash_from_cid(&app_data_cid(default)).unwrap(),
default
);
for i in 0..32 {
let mut bytes = [0u8; 32];
bytes[i] = 0xff;
let hash = AppDataHash::from(bytes);
let cid = app_data_cid(hash);
let rendered = cid.to_string();
assert_eq!(
app_data_hash_from_cid(&cid).unwrap(),
hash,
"round-trip failed at byte {i}"
);
assert!(rendered.starts_with('b'));
assert!(
rendered
.chars()
.skip(1)
.all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c))
);
}
}
#[test]
fn cid_for_empty_app_data_hash_starts_with_bafkrw() {
let cid = app_data_cid(EMPTY_APP_DATA_HASH).to_string();
assert!(
cid.starts_with("bafkrw"),
"expected bafkrw prefix, got {cid}"
);
}
#[test]
fn cid_matches_services_known_good_vector() {
let hash = b256!("8af4e8c9973577b08ac21d17d331aade86c11ebcc5124744d621ca8365ec9424");
let cid = app_data_cid(hash);
assert_eq!(
cid.to_string(),
"bafkrwiek6tumtfzvo6yivqq5c7jtdkw6q3ar5pgfcjdujvrbzkbwl3eueq"
);
assert_eq!(app_data_hash_from_cid(&cid).unwrap(), hash);
}
#[test]
fn cid_for_empty_doc_golden() {
let cid = app_data_cid(EMPTY_APP_DATA_HASH);
assert_eq!(
cid.to_string(),
"bafkrwifuru4pspvkbbadh7czoc7znzkzym6ezxah3ce2wafu2y7zledttu"
);
}
#[test]
fn cid_parse_rejects_missing_multibase_prefix() {
let err = "afkrwifuru4pspvkbbadh7czoc7znzkzym6ezxah3ce2wafu2y7zledttu"
.parse::<AppDataCid>()
.unwrap_err();
assert!(matches!(err, ::cid::Error::ParsingError), "got: {err:?}");
}
#[test]
fn cid_parse_accepts_base16_multibase_prefix() {
let hash = b256!("8af4e8c9973577b08ac21d17d331aade86c11ebcc5124744d621ca8365ec9424");
let mut hex_body = String::with_capacity(72);
hex_body.push_str("01551b20");
hex_body.push_str(&const_hex::encode(hash));
let cid = format!("f{hex_body}").parse::<AppDataCid>().unwrap();
assert_eq!(app_data_hash_from_cid(&cid).unwrap(), hash);
}
#[test]
fn cid_parse_rejects_invalid_base16_body() {
let err = "f01551b20zzzz".parse::<AppDataCid>().unwrap_err();
assert!(matches!(err, ::cid::Error::ParsingError), "got: {err:?}");
}
#[test]
fn parse_app_data_cid_rejects_oversize_string() {
let oversize = format!("b{}", "a".repeat(MAX_CID_STR_LEN));
let err = parse_app_data_cid(&oversize).unwrap_err();
assert!(
matches!(err, AppDataCidError::CidTooLong { max, .. } if max == MAX_CID_STR_LEN),
"got: {err:?}"
);
}
#[test]
fn parse_app_data_cid_accepts_canonical_string() {
let cid = app_data_cid(EMPTY_APP_DATA_HASH).to_string();
assert!(cid.len() <= MAX_CID_STR_LEN);
let parsed = parse_app_data_cid(&cid).unwrap();
assert_eq!(
app_data_hash_from_cid(&parsed).unwrap(),
EMPTY_APP_DATA_HASH
);
}
#[test]
fn cid_parse_rejects_wrong_codec() {
let multihash =
Multihash::<32>::wrap(MULTIHASH_KECCAK_256, EMPTY_APP_DATA_HASH.as_slice()).unwrap();
let cid = AppDataCid::new_v1(0x70, multihash.resize().unwrap());
let err = app_data_hash_from_cid(&cid).unwrap_err();
assert!(
matches!(err, AppDataCidError::UnexpectedCodec(0x70)),
"got: {err:?}"
);
}
#[test]
fn cid_parse_rejects_wrong_multihash() {
let multihash = Multihash::<32>::wrap(0x12, EMPTY_APP_DATA_HASH.as_slice()).unwrap();
let cid = AppDataCid::new_v1(CID_CODEC_RAW, multihash.resize().unwrap());
let err = app_data_hash_from_cid(&cid).unwrap_err();
assert!(
matches!(err, AppDataCidError::UnexpectedMultihashCode(0x12)),
"got: {err:?}"
);
}
#[test]
fn cid_parse_rejects_truncated_body() {
let err = "babcdefgh".parse::<AppDataCid>().unwrap_err();
assert!(
matches!(
err,
::cid::Error::ParsingError
| ::cid::Error::VarIntDecodeError
| ::cid::Error::InputTooShort
| ::cid::Error::InvalidExplicitCidV0
| ::cid::Error::Io(_)
),
"got: {err:?}"
);
}
#[test]
fn cid_parse_rejects_wrong_version() {
let bytes = [
0x00, 0x55, 0x1b, 0x20, 0xb4, 0x8d, 0x38, 0xf9, 0x3e, 0xaa, 0x08, 0x40, 0x33, 0xfc, 0x59,
0x70, 0xbf, 0x96, 0xe5, 0x59, 0xc3, 0x3c, 0x4c, 0xdc, 0x07, 0xd8, 0x89, 0xab, 0x00, 0xb4,
0xd6, 0x3f, 0x95, 0x90, 0x73, 0x9d,
];
let encoded = ::cid::multibase::encode(::cid::multibase::Base::Base32Lower, bytes);
let err = encoded.parse::<AppDataCid>().unwrap_err();
assert!(
matches!(err, ::cid::Error::InvalidExplicitCidV0),
"got: {err:?}"
);
}
#[test]
fn cid_parse_rejects_wrong_digest_length() {
let multihash =
Multihash::<32>::wrap(MULTIHASH_KECCAK_256, &EMPTY_APP_DATA_HASH.as_slice()[..16]).unwrap();
let cid = AppDataCid::new_v1(CID_CODEC_RAW, multihash.resize().unwrap());
let err = app_data_hash_from_cid(&cid).unwrap_err();
assert!(
matches!(err, AppDataCidError::UnexpectedDigestLength(16)),
"got: {err:?}"
);
}
#[test]
fn cid_parse_rejects_invalid_base32_char() {
let err = "b8".parse::<AppDataCid>().unwrap_err();
assert!(
matches!(
err,
::cid::Error::ParsingError | ::cid::Error::InputTooShort
),
"got: {err:?}"
);
}