diff --git a/crates/shepherd-sdk/README.md b/crates/shepherd-sdk/README.md new file mode 100644 index 0000000..4726465 --- /dev/null +++ b/crates/shepherd-sdk/README.md @@ -0,0 +1,90 @@ +# shepherd-sdk + +Guest-side SDK for [Shepherd](https://github.com/nullislabs/shepherd) modules. + +`shepherd-sdk` is the shared companion to each module's +`wit_bindgen::generate!` invocation: the module keeps its own +wit-bindgen call (which emits the world-specific `Guest` trait and +host-import shims into the module's own crate) and pulls helpers, +typed primitives, and the host trait seam from here. + +## Quick tour + +```rust +use shepherd_sdk::prelude::*; +use shepherd_sdk::cow::{gpv2_to_order_data, classify_api_error, RetryAction}; +use shepherd_sdk::chain::{eth_call_params, parse_eth_call_result}; +``` + +| Module | What it provides | +|---|---| +| `prelude` | One-liner `use ::*` for alloy primitives + cowprotocol order / signing / orderbook surface. | +| `cow::order` | `gpv2_to_order_data` — `GPv2OrderData` -> typed `OrderData`. | +| `cow::composable` | `sol! IConditionalOrder` errors + `PollOutcome` + `decode_revert`. | +| `cow::error` | `RetryAction` enum + `classify_api_error` + `try_decode_api_error`. | +| `chain::eth_call` | `eth_call_params`, `parse_eth_call_result`, `decode_revert_hex`. | +| `host` | `Host` trait seam (`ChainHost` / `LocalStoreHost` / `CowApiHost` / `LoggingHost`) + host-neutral `HostError`. | + +## Testing modules host-free + +Add the companion `shepherd-sdk-test` crate as a dev-dep and write +your strategy function against `&impl shepherd_sdk::host::Host`: + +```rust,ignore +use shepherd_sdk::host::*; + +pub fn handle_block(host: &H, chain_id: u64) -> Result<(), HostError> { + let result = host.request(chain_id, "eth_blockNumber", "[]")?; + host.log(LogLevel::Info, &format!("got {result}")); + Ok(()) +} +``` + +Tests against `MockHost` then run without `wit-bindgen` or +`wasmtime`: + +```rust,ignore +let host = MockHost::new(); +host.chain.respond_to("eth_blockNumber", "[]", Ok("\"0x1\"".into())); +handle_block(&host, 1).unwrap(); +assert_eq!(host.chain.call_count(), 1); +``` + +## Why no `wit_bindgen::generate!` in the SDK + +The macro emits types into the calling crate (the module's cdylib). +Re-exporting wit-bindgen output from a library would duplicate +symbols and break the component-export contract. Helpers in this +SDK take primitive arguments (`&[u8]`, `&str`, `Option<&str>`) so +the SDK stays world-neutral; modules unpack their wit-bindgen +`HostError` / `Log` into primitives at the call site. Trade-off +documented in ADR-0006 and ADR-0007 in `docs/adr/`. + +## Layout + +``` +crates/shepherd-sdk/ +├── src/ +│ ├── lib.rs crate root + intra-doc links +│ ├── prelude.rs bulk re-exports +│ ├── cow/ +│ │ ├── mod.rs +│ │ ├── order.rs gpv2_to_order_data +│ │ ├── composable.rs IConditionalOrder + PollOutcome + decode_revert +│ │ └── error.rs RetryAction + classify_api_error +│ ├── chain/ +│ │ ├── mod.rs +│ │ └── eth_call.rs eth_call_params + parse_eth_call_result +│ └── host.rs trait seam + SDK HostError +└── README.md you are here +``` + +## Generating docs locally + +```sh +RUSTDOCFLAGS="-D warnings -D missing-docs" cargo doc -p shepherd-sdk --no-deps --open +``` + +The CI gate `cargo doc -p shepherd-sdk --no-deps` runs under those +flags, so all public items carry doc comments and intra-doc links +resolve. diff --git a/crates/shepherd-sdk/src/cow/error.rs b/crates/shepherd-sdk/src/cow/error.rs index 8555884..1874465 100644 --- a/crates/shepherd-sdk/src/cow/error.rs +++ b/crates/shepherd-sdk/src/cow/error.rs @@ -2,12 +2,13 @@ //! //! Maps `cow_api::submit_order` failures into a typed [`RetryAction`] //! the lifecycle layer dispatches on. The orderbook returns a typed -//! [`ApiError`](cowprotocol::error::ApiError) JSON body on permanent -//! / transient failures; the host forwards that JSON in -//! `host-error.data` (once the chain backend supports it — see ADR -//! follow-up). Until then, [`classify_api_error`] falls back to -//! `TryNextBlock` so a flaky orderbook does not poison still-valid -//! orders. +//! [`ApiError`] JSON body on permanent / transient failures; the host +//! forwards that JSON in `host-error.data` (once the chain backend +//! supports it — see the ADR follow-up). Until then, +//! [`classify_api_error`] falls back to `TryNextBlock` so a flaky +//! orderbook does not poison still-valid orders. +//! +//! [`ApiError`]: cowprotocol::error::ApiError use cowprotocol::error::ApiError; diff --git a/crates/shepherd-sdk/src/lib.rs b/crates/shepherd-sdk/src/lib.rs index f09b674..0fde315 100644 --- a/crates/shepherd-sdk/src/lib.rs +++ b/crates/shepherd-sdk/src/lib.rs @@ -9,35 +9,32 @@ //! //! ## What lives here //! -//! - [`prelude`] — `use shepherd_sdk::prelude::*` imports the -//! protocol-level types modules need on every other line: alloy -//! primitives ([`Address`](alloy_primitives::Address), -//! [`B256`](alloy_primitives::B256), -//! [`Bytes`](alloy_primitives::Bytes), -//! [`U256`](alloy_primitives::U256), [`keccak256`]( -//! alloy_primitives::keccak256)) and cowprotocol's order / -//! signing surface ([`OrderCreation`](cowprotocol::OrderCreation), -//! [`OrderData`](cowprotocol::OrderData), -//! [`OrderUid`](cowprotocol::OrderUid), -//! [`OrderKind`](cowprotocol::OrderKind), -//! [`Signature`](cowprotocol::Signature), -//! [`Chain`](cowprotocol::Chain), -//! [`GPv2OrderData`](cowprotocol::GPv2OrderData), -//! [`EMPTY_APP_DATA_JSON`](cowprotocol::EMPTY_APP_DATA_JSON), and -//! the [`ApiError`](cowprotocol::ApiError) + -//! [`OrderPostErrorKind`](cowprotocol::error::OrderPostErrorKind) -//! retry contract). +//! - [`prelude`] — `use shepherd_sdk::prelude::*` imports alloy +//! primitives ([`Address`], [`B256`], [`Bytes`], [`U256`], +//! [`keccak256`]) and cowprotocol's order / signing / orderbook +//! surface ([`OrderCreation`], [`OrderData`], [`OrderUid`], +//! [`OrderKind`], [`Signature`], [`Chain`], [`GPv2OrderData`], +//! [`EMPTY_APP_DATA_JSON`], [`ApiError`], [`OrderPostErrorKind`]). //! -//! - [`cow`] (BLEU-840) — `GPv2OrderData` <-> `OrderData` bridging, -//! `IConditionalOrder` revert decoding, `RetryAction` classifier. -//! Stubbed in this skeleton; populated by the BLEU-840 extraction. +//! - [`cow`] — `GPv2OrderData` -> `OrderData` bridging +//! ([`gpv2_to_order_data`]), `IConditionalOrder` revert decoding +//! ([`PollOutcome`] + [`decode_revert`]), and the +//! [`RetryAction`] classifier driving submit-failure dispatch. //! -//! - [`chain`] (BLEU-840) — `eth_call` JSON plumbing -//! (`eth_call_params`, `parse_eth_call_result`, `decode_revert_hex`). -//! Stubbed in this skeleton; populated by the BLEU-840 extraction. +//! - [`chain`] — `eth_call` JSON plumbing +//! ([`eth_call_params`], [`parse_eth_call_result`], +//! [`decode_revert_hex`]). //! -//! - [`store`] (BLEU-840) — `WatchSet` and `BackoffLedger` helpers -//! per ADR-0006. Stubbed in this skeleton. +//! - [`host`] — host trait seam ([`Host`] / [`ChainHost`] / +//! [`LocalStoreHost`] / [`CowApiHost`] / [`LoggingHost`]) plus a +//! host-neutral [`HostError`]. Modules that want host-free tests +//! structure their strategy logic against these traits and slot +//! in the `shepherd-sdk-test` mocks. See the host module docs for +//! the wit-bindgen adapter pattern. +//! +//! - `store` — placeholder for `WatchSet` / `BackoffLedger` +//! per ADR-0006. Populated when a second strategy module needs +//! the same key conventions. //! //! ## Why no `wit_bindgen::generate!` here //! @@ -49,6 +46,35 @@ //! struct; modules unpack their `HostError` on the way in. Trade-off //! documented in ADR-0006 / ADR-0007 — the SDK stays on the guest //! side, neutral to which world the module exports. +//! +//! [`Address`]: alloy_primitives::Address +//! [`B256`]: alloy_primitives::B256 +//! [`Bytes`]: alloy_primitives::Bytes +//! [`U256`]: alloy_primitives::U256 +//! [`keccak256`]: alloy_primitives::keccak256 +//! [`OrderCreation`]: cowprotocol::OrderCreation +//! [`OrderData`]: cowprotocol::OrderData +//! [`OrderUid`]: cowprotocol::OrderUid +//! [`OrderKind`]: cowprotocol::OrderKind +//! [`Signature`]: cowprotocol::Signature +//! [`Chain`]: cowprotocol::Chain +//! [`GPv2OrderData`]: cowprotocol::GPv2OrderData +//! [`EMPTY_APP_DATA_JSON`]: cowprotocol::EMPTY_APP_DATA_JSON +//! [`ApiError`]: cowprotocol::ApiError +//! [`OrderPostErrorKind`]: cowprotocol::error::OrderPostErrorKind +//! [`gpv2_to_order_data`]: cow::gpv2_to_order_data +//! [`PollOutcome`]: cow::PollOutcome +//! [`decode_revert`]: cow::decode_revert +//! [`RetryAction`]: cow::RetryAction +//! [`eth_call_params`]: chain::eth_call_params +//! [`parse_eth_call_result`]: chain::parse_eth_call_result +//! [`decode_revert_hex`]: chain::decode_revert_hex +//! [`Host`]: host::Host +//! [`ChainHost`]: host::ChainHost +//! [`LocalStoreHost`]: host::LocalStoreHost +//! [`CowApiHost`]: host::CowApiHost +//! [`LoggingHost`]: host::LoggingHost +//! [`HostError`]: host::HostError #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] @@ -58,11 +84,6 @@ pub mod cow; pub mod host; pub mod prelude; -/// `local-store` helpers: `WatchSet`, `BackoffLedger` per ADR-0006. -/// -/// Skeleton — populated by a follow-up to BLEU-840 once a second -/// strategy module needs the same key conventions. -pub mod store {} #[cfg(test)] mod tests { diff --git a/docs/sdk.md b/docs/sdk.md new file mode 100644 index 0000000..d9dbbed --- /dev/null +++ b/docs/sdk.md @@ -0,0 +1,86 @@ +# shepherd-sdk + +`shepherd-sdk` is the guest-side library every Shepherd module +consumes. It provides typed primitives, ABI helpers, an effect-trait +seam for testing, and a `prelude` that keeps boilerplate out of +module crates. + +This page is the entry point. The full API reference is the rustdoc +site under `target/doc/shepherd_sdk/`, generated by: + +```sh +RUSTDOCFLAGS="-D warnings -D missing-docs" cargo doc -p shepherd-sdk --no-deps --open +``` + +## Supported host capabilities + +`shepherd-sdk` is host-neutral — it does not call wit-bindgen- +generated functions directly. Instead, it exposes traits that mirror +the on-the-wire host interfaces, and modules adapt their wit-bindgen +imports to the traits at the cdylib boundary. The traits in +[`shepherd_sdk::host`][host-doc] are: + +| Trait | Mirrors | What it does | +|---|---|---| +| `ChainHost` | `nexum:host/chain@0.2.0` | JSON-RPC dispatch (`eth_call`, `eth_getLogs`, …) | +| `LocalStoreHost` | `nexum:host/local-store@0.2.0` | Per-module key-value store | +| `CowApiHost` | `shepherd:cow/cow-api@0.2.0` | Orderbook submission (`POST /api/v1/orders`) | +| `LoggingHost` | `nexum:host/logging@0.2.0` | Structured log lines tagged by module | +| `Host` | supertrait | Bundles the four; blanket impl | + +A module declaring `[capabilities].required = ["chain", "local-store", +"cow-api", "logging"]` in its `module.toml` matches the host trait +seam one-for-one. + +[host-doc]: ../target/doc/shepherd_sdk/host/index.html + +## Modules + +- [`prelude`](../target/doc/shepherd_sdk/prelude/index.html) — bulk + re-exports. `use shepherd_sdk::prelude::*;` and every module path + resolves: alloy primitives (`Address`, `B256`, `Bytes`, `U256`, + `keccak256`) plus cowprotocol order / signing / orderbook surface. + +- [`cow`](../target/doc/shepherd_sdk/cow/index.html) — CoW Protocol + bridging: + - `cow::order::gpv2_to_order_data` — convert the on-chain + `GPv2OrderData` (12-field Solidity tuple with bytes32 markers) + into the typed `OrderData` shape the orderbook signs against. + - `cow::composable::PollOutcome` + `cow::composable::decode_revert` + — typed dispatch over the five `IConditionalOrder` custom errors + (`OrderNotValid`, `PollTryNextBlock`, `PollTryAtBlock`, + `PollTryAtEpoch`, `PollNever`). + - `cow::error::RetryAction` + `cow::error::classify_api_error` — + map `cow_api::submit_order` failures into `TryNextBlock` / + `Backoff(s)` / `Drop`. + +- [`chain`](../target/doc/shepherd_sdk/chain/index.html) — `eth_call` + JSON plumbing: + - `chain::eth_call_params(to, data)` — build the `[{to, data}, + "latest"]` params array. + - `chain::parse_eth_call_result(json)` — parse the `"0x..."` hex + response into bytes. + - `chain::decode_revert_hex(s)` — `host-error.data` hex blob -> + typed `PollOutcome`. + +- [`host`](../target/doc/shepherd_sdk/host/index.html) — host trait + seam plus the SDK's host-neutral `HostError` (same field shape + as wit-bindgen's, bridged via one-liner `From` impls per module). + +## Companion: shepherd-sdk-test + +Add `shepherd-sdk-test` as a dev-dep on the module crate to write +strategy tests against in-memory mocks. See its +[README](../crates/shepherd-sdk-test/src/lib.rs) for the usage +pattern. + +## Versioning + +The SDK is currently `0.1.0` and lives at `crates/shepherd-sdk/` in +the shepherd monorepo. It is not yet published to crates.io; modules +depend on it via a workspace path. + +The `[patch.crates-io]` at the workspace root pins `cowprotocol` to a +specific commit on `bleu/cow-rs` (per ADR-0004); the SDK rides that +patch transitively, so module Cargo.toml files declare +`cowprotocol = "1.0.0-alpha.3"` and pick up the fork automatically.