Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions crates/shepherd-sdk/README.md
Original file line number Diff line number Diff line change
@@ -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<H: Host>(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.
13 changes: 7 additions & 6 deletions crates/shepherd-sdk/src/cow/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
83 changes: 52 additions & 31 deletions crates/shepherd-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand All @@ -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))]
Expand All @@ -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 {
Expand Down
86 changes: 86 additions & 0 deletions docs/sdk.md
Original file line number Diff line number Diff line change
@@ -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.