diff --git a/crates/shepherd-sdk/src/chain/eth_call.rs b/crates/shepherd-sdk/src/chain/eth_call.rs index 3f3de50..b915f76 100644 --- a/crates/shepherd-sdk/src/chain/eth_call.rs +++ b/crates/shepherd-sdk/src/chain/eth_call.rs @@ -9,6 +9,23 @@ use crate::cow::composable::{PollOutcome, decode_revert}; /// Returned as a `String` rather than `serde_json::Value` so the caller /// can hand it straight to `chain::request(chain_id, "eth_call", &p)` /// without re-serialising. +/// +/// # Example +/// +/// ``` +/// use shepherd_sdk::chain::eth_call_params; +/// use shepherd_sdk::prelude::Address; +/// +/// let to: Address = "0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74" +/// .parse() +/// .unwrap(); +/// let selector = [0xaa, 0xbb, 0xcc, 0xdd]; // 4-byte function selector +/// let params = eth_call_params(&to, &selector); +/// +/// assert!(params.contains("\"to\":\"0xfdafc9d1902f4e0b84f65f49f244b32b31013b74\"")); +/// assert!(params.contains("\"data\":\"0xaabbccdd\"")); +/// assert!(params.contains("\"latest\"")); +/// ``` pub fn eth_call_params(to: &Address, data: &[u8]) -> String { let to_hex = format!("{to:#x}"); let data_hex = alloy_primitives::hex::encode_prefixed(data); @@ -19,6 +36,23 @@ pub fn eth_call_params(to: &Address, data: &[u8]) -> String { /// returns for an `eth_call`. The value is a JSON string holding hex /// like `"0x1234..."`; strip the JSON quotes, strip the `0x` prefix, /// and hex-decode. Returns `None` on shape mismatch. +/// +/// # Example +/// +/// ``` +/// use shepherd_sdk::chain::parse_eth_call_result; +/// +/// // What the host typically returns for an eth_call result: a JSON +/// // string holding 0x-prefixed hex. +/// let raw = r#""0xdeadbeef""#; +/// assert_eq!( +/// parse_eth_call_result(raw), +/// Some(vec![0xde, 0xad, 0xbe, 0xef]), +/// ); +/// +/// // Shape mismatch (not JSON-quoted) -> None. +/// assert_eq!(parse_eth_call_result("not json"), None); +/// ``` pub fn parse_eth_call_result(result_json: &str) -> Option> { let s = serde_json::from_str::(result_json).ok()?; let hex = s.strip_prefix("0x").unwrap_or(&s); @@ -32,6 +66,26 @@ pub fn parse_eth_call_result(result_json: &str) -> Option> { /// This is the bridge between the host's structured error data (a hex /// string in `host-error.data`) and the typed /// [`crate::cow::composable::PollOutcome`] dispatch. +/// +/// # Example +/// +/// ``` +/// use alloy_sol_types::SolError; +/// use shepherd_sdk::chain::decode_revert_hex; +/// use shepherd_sdk::cow::{IConditionalOrder, PollOutcome}; +/// +/// // Simulate the host forwarding an OrderNotValid revert payload. +/// let revert = IConditionalOrder::OrderNotValid { +/// reason: "expired".into(), +/// } +/// .abi_encode(); +/// let host_data = format!("\"0x{}\"", alloy_primitives::hex::encode(&revert)); +/// +/// assert!(matches!( +/// decode_revert_hex(&host_data), +/// Some(PollOutcome::DontTryAgain), +/// )); +/// ``` pub fn decode_revert_hex(s: &str) -> Option { let stripped = s.trim_matches('"'); let stripped = stripped.strip_prefix("0x").unwrap_or(stripped); diff --git a/crates/shepherd-sdk/src/cow/error.rs b/crates/shepherd-sdk/src/cow/error.rs index f8e342c..12c45e9 100644 --- a/crates/shepherd-sdk/src/cow/error.rs +++ b/crates/shepherd-sdk/src/cow/error.rs @@ -54,6 +54,31 @@ pub fn try_decode_api_error(host_error_data: Option<&str>) -> Option { /// - Recognised non-retriable kinds → `Drop`. /// - Payload absent or unparseable → `TryNextBlock` (safe default; a /// flaky orderbook should not be treated as a permanent rejection). +/// +/// # Example +/// +/// ``` +/// use shepherd_sdk::cow::{classify_api_error, RetryAction}; +/// +/// // Transient: orderbook rejects with InsufficientFee -> retry next block. +/// let transient = serde_json::json!({ +/// "errorType": "InsufficientFee", +/// "description": "fee too low", +/// }) +/// .to_string(); +/// assert_eq!(classify_api_error(Some(&transient)), RetryAction::TryNextBlock); +/// +/// // Permanent: InvalidSignature -> drop the watch / placement. +/// let permanent = serde_json::json!({ +/// "errorType": "InvalidSignature", +/// "description": "bad sig", +/// }) +/// .to_string(); +/// assert_eq!(classify_api_error(Some(&permanent)), RetryAction::Drop); +/// +/// // No payload (e.g. host-error.data is None) -> safe default. +/// assert_eq!(classify_api_error(None), RetryAction::TryNextBlock); +/// ``` pub fn classify_api_error(host_error_data: Option<&str>) -> RetryAction { match try_decode_api_error(host_error_data) { Some(api) if api.retry_hint() => RetryAction::TryNextBlock, diff --git a/crates/shepherd-sdk/src/cow/order.rs b/crates/shepherd-sdk/src/cow/order.rs index 6c8c536..177f7fb 100644 --- a/crates/shepherd-sdk/src/cow/order.rs +++ b/crates/shepherd-sdk/src/cow/order.rs @@ -24,6 +24,35 @@ use cowprotocol::{BuyTokenDestination, GPv2OrderData, OrderData, OrderKind, Sell /// from_signed_order_data` does the same downstream, but doing it here /// keeps the EIP-712 hash inputs verbatim if a caller bypasses that /// helper later. +/// +/// # Example +/// +/// ``` +/// use cowprotocol::{ +/// BuyTokenDestination, GPv2OrderData, OrderKind, SellTokenSource, +/// }; +/// use shepherd_sdk::cow::gpv2_to_order_data; +/// use shepherd_sdk::prelude::{Address, U256}; +/// +/// let gpv2 = GPv2OrderData { +/// sellToken: Address::repeat_byte(1), +/// buyToken: Address::repeat_byte(2), +/// receiver: Address::ZERO, // normalised to None +/// sellAmount: U256::from(1_000u64), +/// buyAmount: U256::from(999u64), +/// validTo: u32::MAX, +/// appData: cowprotocol::EMPTY_APP_DATA_HASH, +/// feeAmount: U256::ZERO, +/// kind: OrderKind::SELL, +/// partiallyFillable: false, +/// sellTokenBalance: SellTokenSource::ERC20, +/// buyTokenBalance: BuyTokenDestination::ERC20, +/// }; +/// +/// let order = gpv2_to_order_data(&gpv2).expect("known markers"); +/// assert_eq!(order.sell_amount, U256::from(1_000u64)); +/// assert_eq!(order.receiver, None); +/// ``` pub fn gpv2_to_order_data(gpv2: &GPv2OrderData) -> Option { Some(OrderData { sell_token: gpv2.sellToken, diff --git a/crates/shepherd-sdk/src/host.rs b/crates/shepherd-sdk/src/host.rs index c4f1ece..7eacbfb 100644 --- a/crates/shepherd-sdk/src/host.rs +++ b/crates/shepherd-sdk/src/host.rs @@ -131,5 +131,47 @@ pub trait LoggingHost { /// A blanket impl is provided for any type that implements all four /// component traits, so callers do not have to add a redundant /// `impl Host for MyHost {}`. +/// +/// # Example +/// +/// Strategy functions are generic over [`Host`]. Production code plugs +/// the per-module `WitBindgenHost` adapter (see `modules/examples/`); +/// unit tests plug `shepherd_sdk_test::MockHost`. +/// +/// ``` +/// use shepherd_sdk::host::{ +/// ChainHost, CowApiHost, Host, HostError, LocalStoreHost, LogLevel, LoggingHost, +/// }; +/// +/// /// Pure strategy logic - no wit-bindgen calls in here. +/// fn record_block(host: &H, chain_id: u64, key: &str) -> Result<(), HostError> { +/// host.log(LogLevel::Info, "recording block"); +/// host.set(key, b"")?; +/// let _block_number = host.request(chain_id, "eth_blockNumber", "[]")?; +/// Ok(()) +/// } +/// +/// // Minimal hand-rolled host so the doctest is self-contained. +/// // Real modules wire `shepherd_sdk_test::MockHost` here. +/// # struct StubHost; +/// # impl ChainHost for StubHost { +/// # fn request(&self, _: u64, _: &str, _: &str) -> Result { +/// # Ok("\"0x0\"".into()) +/// # } +/// # } +/// # impl LocalStoreHost for StubHost { +/// # fn get(&self, _: &str) -> Result>, HostError> { Ok(None) } +/// # fn set(&self, _: &str, _: &[u8]) -> Result<(), HostError> { Ok(()) } +/// # fn delete(&self, _: &str) -> Result<(), HostError> { Ok(()) } +/// # fn list_keys(&self, _: &str) -> Result, HostError> { Ok(vec![]) } +/// # } +/// # impl CowApiHost for StubHost { +/// # fn submit_order(&self, _: u64, _: &[u8]) -> Result { Ok("".into()) } +/// # } +/// # impl LoggingHost for StubHost { +/// # fn log(&self, _: LogLevel, _: &str) {} +/// # } +/// record_block(&StubHost, 1, "block:42").unwrap(); +/// ``` pub trait Host: ChainHost + LocalStoreHost + CowApiHost + LoggingHost {} impl Host for T {}