Skip to content

cow: build the cow venue adapter component with timeout transport middleware#467

Open
mfw78 wants to merge 1 commit into
feat/m4-idempotency-seamfrom
feat/m4-cow-adapter-cdylib
Open

cow: build the cow venue adapter component with timeout transport middleware#467
mfw78 wants to merge 1 commit into
feat/m4-idempotency-seamfrom
feat/m4-cow-adapter-cdylib

Conversation

@mfw78

@mfw78 mfw78 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Adds the adapter feature slice to cow-venue: CowAdapter under
#[videre_sdk::venue] decodes CowIntentBody, derives the value-flow
header, and speaks the orderbook REST API over scoped wasi:http. A
signed order posts EIP-1271 (receipt is the canonical 56-byte UID); an
unsigned order posts pre-sign and returns requires-signing carrying
the setPreSignature call. An already-held rejection succeeds with the
client-derived UID; errorType rejections project onto venue-error
through the classification table so the retry hint survives the
collapse.

Chain-edge order assembly (gpv2_to_order_data, order_data_to_body,
order_uid_hex, build_order_creation) moves into a new assembly
slice the adapter owns; shepherd-sdk re-exports it for the legacy
keeper run(), otherwise unchanged. videre-sdk gains TimedFetch,
per-request timeout middleware every adapter request rides. Codec
vectors and header goldens ship under tests/vectors with a
conformance suite. The built component bundles into the shepherd
distribution: engine adapters stanza, Docker/CI wasm build, justfile
target.

Closes #324.

Why

Routes outbound orderbook calls through typed transport wrappers
carrying a per-request timeout, resolving #41 as adapter transport
middleware rather than patching cow_orderbook.rs. The adapter is the
videre:adapter/venue-adapter component the CoW-on-videre split plan
calls for; the legacy keeper path stays on CowApiHost but now shares
the assembly slice. Pool-router wiring is a later port.

Testing

  • cargo nextest run -p cow-venue -p videre-sdk -p shepherd-sdk
  • cargo test --doc
  • cargo build --release --target wasm32-wasip2 -p cow-venue --features adapter
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --all-features -- -D warnings

AI Assistance

Implemented with Claude Code.

…dleware

cow-venue gains the adapter slice: CowAdapter under #[videre_sdk::venue]
decodes CowIntentBody, derives the value-flow header, and speaks the
orderbook REST API over scoped wasi:http. A signed order posts EIP-1271
and its receipt is the canonical 56-byte UID; an unsigned order posts
pre-sign and returns requires-signing carrying the setPreSignature call.
An already-held rejection is success with the client-derived UID, and
errorType rejections project onto venue-error through the shipped
classification table, so the retry hint survives the collapse.

The chain-edge order assembly (gpv2_to_order_data, order_data_to_body,
order_uid_hex, build_order_creation) moves into the new assembly slice
the adapter owns; shepherd-sdk re-exports it for the legacy keeper run.
videre-sdk gains TimedFetch, the per-request timeout middleware every
adapter request rides. Codec vectors and header goldens are published
under tests/vectors with a conformance suite, and the built component
bundles into the shepherd distribution via the engine adapters stanza
(example and docker configs, justfile, Dockerfile, CI wasm build).

@lgahdl lgahdl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough review given the stakes here (real funds/orders flowing through this eventually) — the vast majority of this checks out clean: EIP-1271 signature attachment is correctly wired to the same decoded body throughout, the pre-sign flow's requires-signing carries the real contract address/UID/calldata with no path that marks an order locally 'live' before the host actually sends the tx, TimedFetch genuinely clamps every adapter request with no bypass found, errorType classification correctly preserves retry hints through the collapse and reuses the already-ratified table from #464 unchanged, the assembly slice move is a pure relocation with the legacy keeper and new adapter now genuinely sharing one function rather than two copies, wasi:http is scoped to exactly api.cow.fi, and no unsafe unwraps exist outside test code. A few things worth a look:

.map_err(|e| VenueError::InvalidBody(e.to_string()))?;
let uid = match post_order(fetch, config, &creation)? {
Posted::Accepted(uid) => uid,
Posted::AlreadyHeld => assembly::order_uid(config.chain, &order, owner),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Posted::AlreadyHeld, the returned UID is computed purely client-side (assembly::order_uid) with nothing from the server to cross-check it against — the CoW API's 409/already-held response carries no UID in its body by design, so this is the only lever available. This is inherent to the "already-held is success" design, not something this diff introduced a bug into, but it's worth having status_with (called right after, in the same flow) hard-fail loudly if the returned order's sell/buy token or validTo don't match the just-submitted body, rather than trusting the locally-derived UID alone all the way through. Same consideration applies to the Accepted branch just above (line 171) — nothing asserts the orderbook's actual stored order matches what was locally derived, which would catch an orderbook/library UID-derivation drift immediately instead of silently trusting the server.

impl Refusal {
/// Collapse for call sites where already-held is not a success
/// shape (reads); the orderbook only emits it on submission.
fn into_error(self) -> VenueError {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already_held is correct as success for submit, but this into_error() exists specifically because status/quote reads must NOT treat it as success — nothing at the type level forces a future call site to pick the right interpretation if someone adds a new read-path call to refusal() and forgets .into_error(). Consider splitting into refusal_for_submit/refusal_for_read (or renaming) to make the caller's obligation explicit rather than relying on every future caller remembering which context they're in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants