Skip to content

feat(cow-ext): typed cow-api-error owned by the extension package#216

Open
mfw78 wants to merge 1 commit into
feat/fault-chain-errorfrom
feat/fault-cow-api-error
Open

feat(cow-ext): typed cow-api-error owned by the extension package#216
mfw78 wants to merge 1 commit into
feat/fault-chain-errorfrom
feat/fault-cow-api-error

Conversation

@mfw78

@mfw78 mfw78 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #210; part of the typed-fault train stacked on the epic-80 stack, merges in stack order.

This gives the shepherd:cow/cow-api extension interface a typed error shape instead of the unified host-error envelope. The guest no longer re-parses the orderbook rejection JSON itself; the host parses it once and hands the guest a typed order-rejection, and a submit failure now discriminates transport faults, HTTP failures, and orderbook rejections as distinct cases instead of a single opaque string.

Changes

wit/shepherd-cow/cow-api.wit drops host-error in favour of the shared nexum:host/types.fault, and adds record http-failure { status: u16, body: option<string> }, record order-rejection { status: u16, error-type: string, description: string }, and variant cow-api-error { fault(fault), http(http-failure), rejected(order-rejection) }. Both request and submit-order now return result<string, cow-api-error>.

shepherd-cow-host/src/ext_cow.rs adds cow_error_to_wit and orderbook_error_to_wit projectors so the host maps its own error types onto the three wit cases; the orderbook rejection envelope is parsed once, host-side, into order-rejection rather than being re-parsed by every guest.

shepherd-sdk/src/cow/error.rs mirrors the wit shape as CowApiError (Fault, Http, Rejected) plus HttpFailure and OrderRejection structs, implements HostFault (the Fault case refines to the embedded fault's own label so metrics stay granular), and replaces try_decode_api_error with classify_api_error(&OrderRejection), which now takes the already-typed rejection instead of re-parsing a string.

shepherd-sdk/src/cow/mod.rs and app_data.rs update CowApiHost and resolve_app_data to return CowApiError (a 404 now surfaces as Http { status: 404, .. }).

shepherd-sdk/src/wit_bindgen_macro.rs adds the convert_cow_err conversion ladder, binding to the fault conversion the nexum host macro already emits rather than duplicating it.

modules/twap-monitor, modules/ethflow-watcher, and modules/examples/stop-loss are updated to dispatch on the typed cases: the app-data 404 checks become http.status == 404, and twap-monitor's submit retry classification dispatches on Rejected and otherwise stays transient (TryNextBlock), preserving the old safe default.

shepherd-backtest picks up a shepherd-sdk dependency so its replay fixture can construct the typed 404 case.

Test plan

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-features (all green, including the classify_api_error doctest)
  • Module wasms (example, twap-monitor, ethflow-watcher, stop-loss, and the harness fixtures) rebuilt in-tree against the updated cow-api.wit; the e2e supervisor subset passes against the rebuilt wasm, including twap_monitor_without_cow_extension_fails_to_boot and the e2e_* boot/dispatch tests, so the wit-bindgen adapter link is exercised in a real component
  • cargo build --release --target wasm32-wasip2 for the three consuming modules succeeds against the regenerated bindings

Notes for reviewer

Only CowApiError::Rejected classifies via error_type in the retry policy; transport Faults and raw Http failures fall through to TryNextBlock at both call sites, matching the old classify_api_error(None) -> TryNextBlock safe default. This was inlined as a two-line match rather than a new helper, so the SDK surface stays exactly classify_api_error(&OrderRejection) as specced.

convert_fault is not duplicated in the cow bind macro; it binds to the conversion the nexum host macro already emits, since a per-extension copy would drift from the shared vocabulary. nexum-sdk's own WIT and macro are untouched by this change, which is the extension-seam property the issue asks to prove.

order-rejection drops the orderbook ApiError.data field the old envelope carried; the retry classifier never consulted it, and the wit shape in the issue defines only status/error-type/description.

Follow-ups intentionally left out of this PR: refreshing the cow-api sequence diagrams and ADRs, which still show the old result<string, host-error> / try_decode_api_error flow; a retry_after_ms-aware RetryAction::Backoff producer, since Fault::RateLimited now carries structured backoff guidance end to end but nothing consumes it yet; and projecting host-side reqwest timeout and 429 responses onto Fault::Timeout / Fault::RateLimited rather than the current blanket Fault::Unavailable.

AI Assistance: Claude Code (Opus 4.8 implementation and adversarial review, Sonnet 5 PR authoring) used for the full change.

Replace the unified host-error envelope on shepherd:cow/cow-api with a
typed cow-api-error variant built on the shared nexum:host/types fault
vocabulary. The interface gains http-failure (raw non-2xx, foreign
orderbook JSON kept as a string) and order-rejection (status, error-type,
description parsed once host-side), and request/submit-order return
result over cow-api-error.

ext_cow projects the orderbook client errors into the typed cases: a
rejection envelope is decoded once into order-rejection, so the guest
never re-parses a failure body. shepherd-sdk mirrors CowApiError with a
HostFault impl that recovers the embedded fault and a stable label, and
classify_api_error now takes an OrderRejection. The cow bind macro grows
the fault and cow-api-error conversion ladder. ethflow-watcher and
twap-monitor match http.status == 404 for the indexer-lag and
appData-not-mirrored checks, and the stop-loss submit path dispatches on
the typed rejection.

This proves the extension seam: nexum WIT is untouched; the richer error
lives entirely in the shepherd:cow package and its SDK.
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.

1 participant