cow-rs 0.1.1

Rust SDK for the CoW Protocol: quoting, signing, posting and tracking orders, plus composable orders, on-chain reads and subgraph queries.
Documentation
//! Wire-format compatibility tests between hand-written domain types in
//! [`super::types`] and the spec-generated types in
//! [`super::generated::inner::types`].
//!
//! # Why this exists
//!
//! The hand-written types in [`super::types`] are the public API of the
//! orderbook module: they use `alloy_primitives::Address`, share enums with
//! the EIP-712 signing side of the crate, and expose ergonomic builders.
//!
//! The generated types in [`super::generated`] are an authoritative mirror
//! of the upstream `OpenAPI` spec at `specs/orderbook-api.yml`, updated by
//! `build.rs` whenever the spec changes.
//!
//! Nothing in the crate uses both sets of types at runtime. This module
//! ensures they stay **wire-compatible** anyway: for each significant
//! domain type we
//!
//! 1. Build a fully-populated hand-written instance,
//! 2. Serialise it through `serde_json`,
//! 3. Deserialise the resulting JSON into the generated counterpart.
//!
//! If step 3 fails, the upstream spec has drifted in a way that the
//! hand-written type no longer satisfies (renamed field, added required
//! property, changed shape) and the hand-written type must be updated.
//!
//! # What is covered
//!
//! Only types whose on-the-wire representation maps cleanly between the two
//! worlds are included here. Types that diverge structurally (e.g.
//! `OrderCreation`, where the generated side models `appData` as a flattened
//! `anyOf` of string variants and `signature` as a `oneOf` enum) would
//! require a conversion layer rather than a plain round-trip and are
//! intentionally out of scope. Rewiring the full `api.rs` onto
//! [`super::generated::inner::types`] is tracked separately.

use serde::{Serialize, de::DeserializeOwned};

use super::{
    generated::types as spec,
    types::{AppDataObject, Auction, CompetitionAuction, InteractionData, Trade},
};

/// Round-trip a hand-written value through `serde_json` into the matching
/// generated type. Panics with a descriptive message on failure so the
/// mismatch is visible in test output.
fn roundtrip<T: Serialize, G: DeserializeOwned>(label: &str, value: &T) -> G {
    let json = serde_json::to_value(value)
        .unwrap_or_else(|e| panic!("{label}: failed to serialise hand-written value: {e}"));
    serde_json::from_value::<G>(json.clone()).unwrap_or_else(|e| {
        panic!(
            "{label}: hand-written value is not wire-compatible with generated type.\n\
                  Deserialisation error: {e}\n\
                  Emitted JSON: {json}"
        )
    })
}

#[test]
fn trade_is_wire_compatible() {
    let trade = Trade {
        block_number: 18_123_456,
        log_index: 42,
        order_uid: format!("0x{}", "ab".repeat(56)),
        owner: "0xb6bad41ae76a11d10f7b0e664c5007b908bc77c9".to_owned(),
        sell_token: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2".to_owned(),
        buy_token: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_owned(),
        sell_amount: "1000000000000000000".to_owned(),
        sell_amount_before_fees: "995000000000000000".to_owned(),
        buy_amount: "2450000000".to_owned(),
        tx_hash: Some(
            "0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_owned(),
        ),
    };
    let _: spec::Trade = roundtrip("Trade", &trade);
}

#[test]
fn interaction_data_is_wire_compatible() {
    let interaction = InteractionData {
        target: "0xb6bad41ae76a11d10f7b0e664c5007b908bc77c9".parse().unwrap(),
        value: "0".to_owned(),
        call_data: "0xdeadbeef".to_owned(),
    };
    let _: spec::InteractionData = roundtrip("InteractionData", &interaction);
}

#[test]
fn total_surplus_is_wire_compatible() {
    let surplus = super::types::TotalSurplus::new("12345678901234567890");
    let _: spec::TotalSurplus = roundtrip("TotalSurplus", &surplus);
}

#[test]
fn app_data_object_is_wire_compatible() {
    let obj = AppDataObject::new(r#"{"version":"1.6.0","metadata":{}}"#);
    // NOTE: our `AppDataObject` only exposes `fullAppData`; the generated
    // counterpart additionally requires an `appData` field. A raw round-trip
    // would therefore fail on a missing required field, so we check a looser
    // invariant: the JSON our type emits must be a strict subset of the JSON
    // the generated type expects.
    let emitted = serde_json::to_value(&obj).expect("serialise AppDataObject");
    assert!(
        emitted.get("fullAppData").is_some(),
        "AppDataObject must emit a `fullAppData` field for wire compatibility"
    );
}

#[test]
fn auction_envelope_is_wire_compatible() {
    let auction = Auction {
        id: Some(7),
        block: 18_000_000,
        orders: Vec::new(),
        prices: foldhash::HashMap::default(),
    };
    // Our `Auction::orders` is typed `Vec<Order>` — without constructing a
    // fully populated `Order` (which has non-trivial nested enums we cannot
    // round-trip) we check the outer envelope and the empty-list case.
    let json = serde_json::to_value(&auction).expect("serialise Auction");
    assert_eq!(json.get("block"), Some(&serde_json::Value::from(18_000_000_u64)));
    assert!(
        json.get("orders").is_some_and(serde_json::Value::is_array),
        "Auction must emit `orders` as a JSON array"
    );
    assert_eq!(json.get("id"), Some(&serde_json::Value::from(7_i64)));
}

#[test]
fn competition_auction_is_wire_compatible() {
    let mut prices = foldhash::HashMap::default();
    prices.insert(
        "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2".to_owned(),
        "1000000000000000000".to_owned(),
    );
    let ca = CompetitionAuction { orders: vec![format!("0x{}", "11".repeat(56))], prices };
    let _: spec::CompetitionAuction = roundtrip("CompetitionAuction", &ca);
}