cowprotocol 1.0.0-alpha.1

Rust SDK for CoW Protocol: orderbook client, EIP-712 order types, signing, and composable-order primitives.
docs.rs failed to build cowprotocol-1.0.0-alpha.1
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

cow-rs

crates.io docs.rs CI Licence: GPL-3.0-or-later

The Rust SDK for the CoW Protocol.

cow-rs is the idiomatic Rust client for trading on CoW Protocol: build and sign orders, talk to the orderbook, decode on-chain settlement events. Built on alloy; ports the canonical types from cowprotocol/services; locks every protocol-critical path byte-for-byte against @cowprotocol/cow-sdk, cowdao-grants/cow-py and ethers.

At a glance

  • Full order lifecycle: quote, sign, submit, look up, cancel.
  • All four signing schemes: EIP-712, EthSign, EIP-1271, pre-sign.
  • All eleven chains: Mainnet, BNB, Gnosis, Polygon, Base, Plasma, Arbitrum One, Avalanche, Ink, Linea, Sepolia, plus their barn staging endpoints where the orderbook team publishes them.
  • Conformance-locked: 219 native tests (180 lib + 26 wiremock + 5 schema-drift + 3 source-lock + 1 trading-mock + 4 doctests) plus 12 headless-Firefox wasm-bindgen cases, with byte-exact goldens cross-checked against cowprotocol/services, cowprotocol/contracts, ethers, cow-sdk and cow-py.
  • Hostile-orderbook hardened: every quote response is bound to the originating QuoteRequest before the SDK produces signable bytes, app-data digests round-trip on get/put, fee-math fails closed via checked_* (no saturation), and EthFlowOrder::receiver is non-zero by construction.
  • Sync core, async client: hashing, signing and contract decoding are pure-compute and need no runtime; the HTTP client is async-tokio and the only piece that depends on one.
  • WASM-ready: compiles cleanly to wasm32-unknown-unknown and has an in-browser end-to-end harness (see test-harness/) that exercises the live orderbook from the browser; the poll helper is runtime-agnostic so you can drop in gloo_timers::future::sleep.

Install

[dependencies]
cowprotocol = "1.0.0-alpha.1"

The crate is published as cowprotocol on crates.io (the cow-rs name was already taken on crates.io by an unrelated publisher before this SDK existed); the source lives at cowdao-grants/cow-rs.

MSRV 1.91, edition 2024.

Quick start: quote, sign, submit

use cowprotocol::{
    Chain, DomainSeparator, EcdsaSigningScheme, EMPTY_APP_DATA_HASH,
    EMPTY_APP_DATA_JSON, OrderBookApi, OrderCreation, QuoteRequest,
};
use alloy_primitives::{U256, address};
use alloy_signer_local::PrivateKeySigner;

# async fn run(signer: PrivateKeySigner) -> cowprotocol::Result<()> {
let api = OrderBookApi::new(Chain::Mainnet);

// 1. Quote.
let request = QuoteRequest::sell_amount_before_fee(
    address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
    address!("6B175474E89094C44Da98b954EedeAC495271d0F"), // DAI
    signer.address(),
    U256::from(100_000_000_u64),
);
let quote = api.get_quote(&request).await?;

// 2. Build the order, sign it under the chain's GPv2Settlement domain.
//    The SDK cross-checks the response against `request` (sellToken,
//    buyToken, receiver, from, kind, plus any field the caller pinned)
//    and refuses to project mismatched fields into signable bytes.
let order_data = quote.to_signed_order_data(&request, EMPTY_APP_DATA_HASH)?;
let domain = DomainSeparator::new(Chain::Mainnet.id(), Chain::Mainnet.settlement());
let signature = order_data.sign(EcdsaSigningScheme::Eip712, &domain, &signer)?;

// 3. Submit.
let creation = OrderCreation::from_signed_order_data(
    order_data,
    signature,
    signer.address(),
    EMPTY_APP_DATA_JSON.to_owned(),
    Some(quote.id),
)?;
let uid = api.post_order(&creation).await?;
println!("https://explorer.cow.fi/orders/{uid}");
# Ok(()) }

See examples/post_order.rs for the same flow on Sepolia, runnable with a private key in the environment.

No-async core

Every protocol-critical primitive is synchronous and runtime-free: OrderData::hash_struct, OrderData::uid, EcdsaSignature::sign, Signature::recover, DomainSeparator::new, the sol!-generated contract bindings. You can use cow-rs in a Postgres extension (pgrx), an FFI shim, an embedded context, or anywhere else a tokio reactor is hostile, without pulling in reqwest or tokio.

use cowprotocol::{OrderBuilder, OrderKind, DomainSeparator, Chain};
use alloy_primitives::{U256, address};

let order = OrderBuilder::new(
    address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
    address!("6B175474E89094C44Da98b954EedeAC495271d0F"),
)
.sell_amount(U256::from(100_000_000_u64))
.buy_amount(U256::from(99_000_000_000_000_000_000_u128))
.valid_to(u32::MAX)
.kind(OrderKind::Sell)
.build();

let domain = DomainSeparator::new(Chain::Mainnet.id(), Chain::Mainnet.settlement());
let owner = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
let uid = order.uid(&domain, owner);
assert_eq!(uid.0.len(), 56);

Modules

Module What it exposes
order OrderData (12-field signed payload), OrderBuilder, OrderUid, OrderKind, SellTokenSource, BuyTokenDestination, BUY_ETH_ADDRESS, plus the full GET-orders Order, OrderStatus, OrderClass
order_book OrderBookApi with quote / submit / lookup / status / cancel, trades, native price, account orders, app-data PUT / GET (with digest round-trip), version, total surplus; the runtime-agnostic poll_until helper and the tokio-bound wait_for_order_fulfilled convenience
trading TradingClient::post_swap_order, the one-call quote → bind → sign → put-app-data → submit facade. Mirrors TradingSdk.postSwapOrder in @cowprotocol/cow-sdk
quote_amounts compute() (the partner-fee + protocol-fee + slippage composition the TS SDK uses, byte-for-byte against cow-sdk PR #867); fail-closed via Error::QuoteFeeMathOverflow { stage } on every intermediate
signature Signature (all four schemes), EcdsaSignature, Recovered, SignatureError
domain DomainSeparator, hashed_eip712_message, hashed_ethsign_message
chain Chain (eleven networks) with orderbook_base_url, orderbook_barn_url, settlement, vault_relayer, subgraph_studio_url
cancellation OrderCancellation (single), OrderCancellations (collection), SignedOrderCancellations
app_data AppDataHash, AppDataDoc (canonical JSON + keccak digest), AppDataCid (IPFS CIDv1 derivation), AppDataDoc::sdk_attribution for the SDK's appCode tag
eth_flow EthFlowOrder (non-zero receiver enforced at construction), ETH_FLOW_PRODUCTION, ETH_FLOW_STAGING
composable ConditionalOrderParams, Proof, PollOutcome, ComposableCoW events, TwapData + TwapStaticInput, plus deployment addresses
multiplexer OZ-style commutative double-hashed merkle leaves, watch-tower-side proof verification
contracts GPv2Settlement (settle + events), CoWSwapOnchainOrders (ETH-flow events), ERC20, WETH9, GPV2_SETTLEMENT, GPV2_VAULT_RELAYER
subgraph SubgraphClient typed access to CoW's subgraph; totals, daily / hourly volume; opt-in bearer-token auth for the gateway URL

Everything is re-exported at the crate root: use cowprotocol::....

WASM and JavaScript

cow-rs targets wasm32-unknown-unknown:

  • Reqwest's browser fetch backend kicks in automatically on wasm.
  • OrderBookApi::poll_until is runtime-agnostic; pair it with gloo_timers::future::sleep instead of tokio::time::sleep.
  • wait_for_order_fulfilled (the tokio-bound convenience) is non-wasm only.
  • CI gates cargo check --target wasm32-unknown-unknown on every push.
  • crates/cow-sdk-wasm/ ships a #[wasm_bindgen] shim published to npm as @cowdao-grants/cow-sdk-wasm; test-harness/index.html exercises it end-to-end against the live orderbook from a real browser. Run with just wasm-harness.

JavaScript quick-start

Two signing flows. Pick one.

In-shim signing (tests, scripts, fast iteration): the wasm crate holds the private key and signs inside linear memory. Requires the in_shim_signing cargo feature at build time (off by default).

import init, {
  get_quote_simple,
  sign_eip712,
  build_order_creation,
  post_order,
} from '@cowdao-grants/cow-sdk-wasm';

await init();

// 1. Quote (network).
const { response } = await get_quote_simple(
  'mainnet',
  '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
  '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
  '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // owner
  '100000000', // 100 USDC, 6 decimals
);

// 2. Sign in-shim.
const sig = sign_eip712(response.quote, 'mainnet', PRIVATE_KEY_HEX);

// 3. Submit (network).
const creation = build_order_creation(
  response.quote, sig, response.from, '{}', response.id,
);
const uid = await post_order('mainnet', creation);
console.log(`https://explorer.cow.fi/orders/${uid}`);

External signing (production, Safe / WalletConnect / browser wallets): the wasm crate never sees the private key. The shim's eip712_payload(orderData, chain) returns a ready-to-use { domain, primaryType, types, message } object — the exact shape both viem and ethers's signTypedData accept. Works against the default (no-feature) build.

import init, {
  eip712_payload,
  get_quote_simple,
  build_order_creation,
  post_order,
} from '@cowdao-grants/cow-sdk-wasm';

await init();

const { response } = await get_quote_simple(
  'mainnet',
  '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  '0x6B175474E89094C44Da98b954EedeAC495271d0F',
  ACCOUNT,
  '100000000',
);
const payload = eip712_payload(response.quote, 'mainnet');

Hand payload to whichever wallet you have. Split the resulting 65-byte signature into (r, s, v) and feed it back through build_order_creation.

// viem
import { hexToBytes } from 'viem';
const signatureHex = await walletClient.signTypedData({ account: ACCOUNT, ...payload });

// ethers v6
const signatureHex = await ethersSigner.signTypedData(
  payload.domain, payload.types, payload.message,
);

// raw EIP-1193 (window.ethereum, WalletConnect, Safe SDK): viem and
// ethers both throw if `types` contains an `EIP712Domain` entry, so
// the shim deliberately omits it. The raw `eth_signTypedData_v4` RPC
// needs it, so inject before stringifying:
const v4 = {
  ...payload,
  types: {
    EIP712Domain: [
      { name: 'name',              type: 'string'  },
      { name: 'version',           type: 'string'  },
      { name: 'chainId',           type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    ...payload.types,
  },
};
const signatureHex = await window.ethereum.request({
  method: 'eth_signTypedData_v4',
  params: [ACCOUNT, JSON.stringify(v4)],
});

// any path → (r, s, v)
const bytes = hexToBytes(signatureHex); // or ethers.getBytes
const sig = {
  signingScheme: 'eip712',
  r: '0x' + Buffer.from(bytes.slice(0, 32)).toString('hex'),
  s: '0x' + Buffer.from(bytes.slice(32, 64)).toString('hex'),
  v: bytes[64],
};

const creation = build_order_creation(response.quote, sig, ACCOUNT, '{}', response.id);
const uid = await post_order('mainnet', creation);

Conformance. eip712_payload produces the digest OrderData::hash_struct computes server-side; the Rust test suite already locks that hash byte-for-byte against ethers's TypedDataEncoder on all eleven chains. The in-browser harness (test-harness/index.html, panel 3) re-asserts the equality at runtime across the shim, viem, and ethers. @cowprotocol/cow-sdk's own typed-data path delegates to ethers's TypedDataEncoder, so parity with cow-sdk follows transitively.

For Safe wallets, replace signTypedData with the Safe SDK's signMessage flow and use build_order_creation_eip1271 instead; the shim wraps the bytes into a Signature::Eip1271 envelope.

See crates/cow-sdk-wasm/README.md for the full exported function list, the in_shim_signing feature trade-off, and the npm publish flow for maintainers.

Build targets and bundle size

wasm-pack produces a different JS glue per target, with the same underlying .wasm binary. The release recipes are:

Target Recipe Output dir Consumers
web just wasm-build-web pkg-web/ Browser ES modules.
bundler just wasm-build-bundler pkg-bundler/ webpack / Vite / Rollup.
nodejs just wasm-build-nodejs pkg-nodejs/ Node 18+, CommonJS.

just wasm-build-all builds all three; just wasm-size reports the post-wasm-opt .wasm byte counts. The wasm binary is byte-identical across targets, so an eventual npm package can ship one .wasm plus three JS glues with an exports map.

Size knobs applied (crates/cow-sdk-wasm/Cargo.toml + workspace [profile.release]):

  • wasm-opt -Oz over the default -O: binaryen biases for binary size (~30% smaller).
  • [profile.release]: lto = "fat", opt-level = "z", panic = "abort", strip = true. Workspace-only — crates.io consumers use their own profile.
  • cowprotocol = { default-features = false }: drops the subgraph GraphQL client; not reachable from JS.
  • lol_alloc global allocator (~5 KB vs dlmalloc's ~10 KB). Active by default — mod allocator; is wired in at the top of the wasm crate's lib.rs.
  • in_shim_signing cargo feature, default-off: gates alloy-signer + alloy-signer-local so the default build ships hash builders only. Saves ~68 KB; integrators sign with viem / ethers / Safe and submit the (r, s, v) bag back through build_order_creation.
  • No reqwest in the wasm output: HTTP-touching exports (get_quote, post_order, etc.) call the JS fetch global directly via js_sys::Reflect, side-stepping reqwest's 150 KB bundle. With lto = "fat" the linker drops reqwest from the wasm binary because no wasm-bindgen export reaches it. Cowprotocol stays unchanged — the same crate continues to ship reqwest-backed OrderBookApi for native consumers.

Current .wasm after wasm-opt -Oz:

default features              584 KB
+ --features in_shim_signing  ~650 KB

(Down from 652 KB / 720 KB before lever 1.)

Conformance

cow-rs locks byte-exact equivalence on every protocol-critical path:

  • ethers TypedDataEncoder for all eleven chains: signed UID, struct hash, domain separator, six order-shape permutations
  • cowprotocol/services for signature recovery and cancellation struct hashes
  • cowprotocol/contracts for the canonical TYPE_HASH derivation, packOrderUidParams layout, and event topic hashes
  • ethers Wallet.signTypedData for the ECDSA (r, s, v) golden
  • cow-py for the ConditionalOrder leaf-id derivation
  • The empty-document app-data digest keccak256("{}")

Regenerate the cross-implementation vectors with:

cd tools/vector-gen && npm install && npm run gen > vectors.json

Status

1.0.0-alpha: the public API is locked unless a critical conformance issue forces a break. Patch releases (1.0.0-alpha.N) bring additive features and bug fixes; breaking changes only on minor or major bumps.

Production readiness:

  • ✅ Byte-conformance with services / contracts / cow-sdk / cow-py
  • ✅ All eleven documented chains
  • ✅ All four signing schemes
  • ✅ Mock-server integration coverage for every OrderBookApi method
  • ✅ WASM compilation gate in CI plus an in-browser e2e harness
  • cargo clippy -- -Dwarnings, no unsafe, no anyhow in lib code
  • ✅ Published to crates.io as cowprotocol

Building

just build        # cargo build --all-targets --all-features --workspace
just test         # cargo test --all-targets --all-features --workspace
just clippy       # cargo clippy ... -- -Dwarnings
just fmt-check
just wasm-check   # cargo check --target wasm32-unknown-unknown ...
just wasm-harness # build cow-sdk-wasm and serve test-harness/ on :8765
just doc          # cargo doc with -D warnings

Layout

crates/cowprotocol/                # Library crate; everything re-exported from the root
crates/cowprotocol/examples/       # get_quote.rs, post_order.rs
crates/cow-sdk-wasm/           # #[wasm_bindgen] shim driving the in-browser harness (unpublished)
test-harness/                 # Static HTML harness; `just wasm-harness` to run
tools/vector-gen/             # Node.js golden-vector generator (ethers reference)

Contributing

See CONTRIBUTING.md. Briefly: Oxford English in prose, no em dashes, Conventional Commits, AI-assistance disclosure in the PR body (never in commits), PRs ≤ 1,500 LoC against develop.

Licence

GPL-3.0-or-later; see LICENSE. Portions adapted from cowprotocol/services under MIT / Apache-2.0 with attribution in each affected file.