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
54 changes: 54 additions & 0 deletions crates/shepherd-sdk/src/chain/eth_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<Vec<u8>> {
let s = serde_json::from_str::<String>(result_json).ok()?;
let hex = s.strip_prefix("0x").unwrap_or(&s);
Expand All @@ -32,6 +66,26 @@ pub fn parse_eth_call_result(result_json: &str) -> Option<Vec<u8>> {
/// 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<PollOutcome> {
let stripped = s.trim_matches('"');
let stripped = stripped.strip_prefix("0x").unwrap_or(stripped);
Expand Down
25 changes: 25 additions & 0 deletions crates/shepherd-sdk/src/cow/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ pub fn try_decode_api_error(host_error_data: Option<&str>) -> Option<ApiError> {
/// - 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,
Expand Down
29 changes: 29 additions & 0 deletions crates/shepherd-sdk/src/cow/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderData> {
Some(OrderData {
sell_token: gpv2.sellToken,
Expand Down
42 changes: 42 additions & 0 deletions crates/shepherd-sdk/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H: Host>(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<String, HostError> {
/// # Ok("\"0x0\"".into())
/// # }
/// # }
/// # impl LocalStoreHost for StubHost {
/// # fn get(&self, _: &str) -> Result<Option<Vec<u8>>, HostError> { Ok(None) }
/// # fn set(&self, _: &str, _: &[u8]) -> Result<(), HostError> { Ok(()) }
/// # fn delete(&self, _: &str) -> Result<(), HostError> { Ok(()) }
/// # fn list_keys(&self, _: &str) -> Result<Vec<String>, HostError> { Ok(vec![]) }
/// # }
/// # impl CowApiHost for StubHost {
/// # fn submit_order(&self, _: u64, _: &[u8]) -> Result<String, HostError> { 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<T: ChainHost + LocalStoreHost + CowApiHost + LoggingHost> Host for T {}