Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,15 @@ jobs:
with:
tool: wasm-tools,ripgrep
- run: ./scripts/check-venue-agnostic.sh

# Blocking orderbook-only gate: the CoW venue crate carries no
# composable symbol (scripts/check-cow-orderbook-only.sh).
cow-orderbook-only:
name: cow-orderbook-only
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2
with:
tool: ripgrep
- run: ./scripts/check-cow-orderbook-only.sh
16 changes: 15 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"crates/composable-cow",
"crates/cow-venue",
"crates/nexum-cli",
"crates/nexum-launch",
Expand Down
24 changes: 24 additions & 0 deletions crates/composable-cow/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "composable-cow"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "ComposableCoW keeper machinery: the conditional-order body and the structured poll Verdict, with the deployed 1.x revert decoding quarantined behind LegacyRevertAdapter."

[lib]
# Plain library, keeper-side only. The CoW venue crate is orderbook-only
# and never links this.

[lints]
workspace = true

[dependencies]
alloy-primitives.workspace = true
alloy-sol-types.workspace = true
borsh.workspace = true
cowprotocol = { version = "0.2.0", default-features = false }
nexum-sdk = { path = "../nexum-sdk" }

[dev-dependencies]
proptest.workspace = true
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
//! The venue-neutral composable (conditional) order body.
//! The composable (conditional) order body.
//!
//! ComposableCoW expresses a conditional order as the
//! `ConditionalOrderParams` tuple: the handler contract that mints the
//! tradeable order, a salt that distinguishes otherwise-identical
//! conditional orders, and the opaque handler-specific static input. This
//! body type is that tuple in wire form. The one non-obvious invariant:
//! `static_input` is opaque to the venue; only the named handler parses
//! conditional orders, and the opaque handler-specific static input.
//! This body type is that tuple in wire form. The one non-obvious
//! invariant: `static_input` is opaque; only the named handler parses
//! it, so this crate never inspects its bytes.

use alloc::vec::Vec;

use borsh::{BorshDeserialize, BorshSerialize};

use crate::order::Address;

/// The venue-neutral conditional order body: `ConditionalOrderParams` in
/// wire form.
/// The conditional order body: `ConditionalOrderParams` in wire form.
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct ComposableBody {
/// The `IConditionalOrder` handler that mints the tradeable order.
pub handler: Address,
pub handler: [u8; 20],
/// Salt distinguishing otherwise-identical conditional orders.
pub salt: [u8; 32],
/// Handler-specific static input; opaque to the venue.
/// Handler-specific static input; opaque to everything but the
/// named handler.
pub static_input: Vec<u8>,
}

Expand Down Expand Up @@ -53,6 +49,9 @@ mod tests {
let mut body = sample();
body.static_input = Vec::new();
let bytes = borsh::to_vec(&body).expect("encode");
assert_eq!(ComposableBody::try_from_slice(&bytes).unwrap(), body);
assert_eq!(
ComposableBody::try_from_slice(&bytes).expect("decode"),
body
);
}
}
15 changes: 15 additions & 0 deletions crates/composable-cow/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! # composable-cow
//!
//! ComposableCoW keeper machinery, kept out of the CoW venue: the
//! conditional-order body ([`ComposableBody`]) and the structured poll
//! seam ([`Verdict`]), with the deployed 1.x reverting wire quarantined
//! behind [`LegacyRevertAdapter`].

#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![warn(missing_docs)]

pub mod body;
pub mod poll;

pub use body::ComposableBody;
pub use poll::{IConditionalOrder, LegacyRevertAdapter, Verdict};
Original file line number Diff line number Diff line change
Expand Up @@ -355,4 +355,18 @@ mod tests {
Verdict::TryNextBlock { .. }
));
}

use proptest::prelude::*;

proptest! {
/// `decode` on arbitrary revert bytes never panics and returns
/// `None` for inputs shorter than the 4-byte EVM selector.
#[test]
fn decode_never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..64)) {
let outcome = LegacyRevertAdapter::decode(&bytes);
if bytes.len() < 4 {
prop_assert!(outcome.is_none());
}
}
}
}
2 changes: 1 addition & 1 deletion crates/cow-venue/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "CoW venue slices. The default `body` slice carries the venue-neutral order/composable intent body types and their borsh IntentBody codec, linkable by adapters and modules."
description = "CoW venue slices, orderbook-only. The default `body` slice carries the venue-neutral order intent body types and their borsh IntentBody codec, linkable by adapters and modules."

[lib]
# Plain library. The default `body` slice is dependency-light so a venue
Expand Down
33 changes: 8 additions & 25 deletions crates/cow-venue/src/body.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! The CoW intent body and its versioned `IntentBody` codec.
//!
//! A CoW intent is either a direct order or a composable (conditional)
//! order; [`CowIntent`] is that sum. [`CowIntentBody`] is the outer
//! per-venue version enum the venue publishes, and `#[derive(IntentBody)]`
//! gives it the borsh codec: a one-byte version tag plus the borsh
//! payload, with an unknown tag failing as a typed
//! A CoW intent is a direct order for the orderbook; [`CowIntent`] is
//! that sum, open for future intent kinds. [`CowIntentBody`] is the
//! outer per-venue version enum the venue publishes, and
//! `#[derive(IntentBody)]` gives it the borsh codec: a one-byte version
//! tag plus the borsh payload, with an unknown tag failing as a typed
//! [`BodyError`](videre_sdk::BodyError) rather than a stringly borsh
//! error. The one non-obvious invariant: the tag order is the schema, so
//! new versions append at the end and no variant is ever reordered or
Expand All @@ -13,16 +13,13 @@
use borsh::{BorshDeserialize, BorshSerialize};
use videre_sdk::IntentBody;

use crate::composable::ComposableBody;
use crate::order::OrderBody;

/// What the CoW venue accepts: a direct order or a conditional order.
/// What the CoW venue accepts: a direct order for the orderbook.
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)]
pub enum CowIntent {
/// A direct `GPv2Order` to place on the orderbook.
Order(OrderBody),
/// A ComposableCoW conditional order that mints tradeable orders.
Composable(ComposableBody),
}

/// The outer per-venue version enum: the schema the CoW venue publishes.
Expand All @@ -49,15 +46,7 @@ mod tests {
.build()
}

fn composable_body() -> ComposableBody {
ComposableBody {
handler: [0xab; 20],
salt: [0xcd; 32],
static_input: vec![9, 8, 7],
}
}

/// The codec conformance set: both v1 intents as round-trip vectors
/// The codec conformance set: the v1 intent as a round-trip vector
/// plus the typed failure contract, in the kit's published form.
fn vectors() -> CodecVectors {
let mut vectors = CodecVectors::new("cow-venue/cow-intent-body");
Expand All @@ -67,12 +56,6 @@ mod tests {
&CowIntentBody::V1(CowIntent::Order(order_body())),
)
.expect("order body encodes");
vectors
.push_round_trip(
"v1-composable",
&CowIntentBody::V1(CowIntent::Composable(composable_body())),
)
.expect("composable body encodes");

let bytes = |intent: CowIntent| CowIntentBody::V1(intent).to_bytes().expect("body encodes");
let mut unknown = bytes(CowIntent::Order(order_body()));
Expand All @@ -90,7 +73,7 @@ mod tests {
truncated,
Expectation::Malformed { version: 0 },
);
let mut trailing = bytes(CowIntent::Composable(composable_body()));
let mut trailing = bytes(CowIntent::Order(order_body()));
trailing.push(0);
vectors.push_failure(
"trailing-bytes",
Expand Down
14 changes: 5 additions & 9 deletions crates/cow-venue/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! # cow-venue
//!
//! The CoW venue, staged as a crate of feature slices. Today only the
//! default [`body`] slice exists: the venue-neutral order and composable
//! intent body types and the borsh `IntentBody` codec over them. The
//! typed client and the adapter component are later slices.
//! The CoW venue, staged as a crate of feature slices: the orderbook
//! and nothing else. The default [`body`] slice carries the
//! venue-neutral order intent body types and the borsh `IntentBody`
//! codec over them; conditional-order keeper machinery lives in its
//! own crate and never here.
//!
//! The body slice is dependency-light on purpose. It links only the
//! venue SDK (for the [`IntentBody`](videre_sdk::IntentBody) derive)
Expand Down Expand Up @@ -35,9 +36,6 @@ extern crate alloc;
#[cfg(feature = "body")]
pub mod body;

#[cfg(feature = "body")]
pub mod composable;

#[cfg(feature = "body")]
pub mod order;

Expand All @@ -57,8 +55,6 @@ pub mod client;
#[cfg(feature = "body")]
pub use body::{CowIntent, CowIntentBody};
#[cfg(feature = "body")]
pub use composable::ComposableBody;
#[cfg(feature = "body")]
pub use order::{
BuyToken, BuyTokenDestination, OrderBody, OrderBuilder, OrderKind, SellToken, SellTokenSource,
};
Expand Down
1 change: 1 addition & 0 deletions crates/shepherd-sdk-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ serde_json = { workspace = true, features = ["std"] }
# Order construction for the MockVenue acceptance tests that drive
# the keeper run end to end.
alloy-primitives.workspace = true
composable-cow = { path = "../composable-cow" }
cowprotocol = { version = "0.2.0", default-features = false }
3 changes: 2 additions & 1 deletion crates/shepherd-sdk-test/tests/mock_venue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
//! strategy code polling the venue directly.

use alloy_primitives::{Address, B256, U256, address, hex, keccak256};
use composable_cow::Verdict;
use cowprotocol::{BuyTokenDestination, GPv2OrderData, OrderKind, SellTokenSource};
use nexum_sdk::host::{Fault, LocalStoreHost as _, RateLimit};
use nexum_sdk::keeper::{ConditionalSource, Journal, Tick, WatchRef, WatchSet, watch_key};
use shepherd_sdk::cow::{CowApiError, CowHost, OrderRejection, Verdict, order_uid_hex, run};
use shepherd_sdk::cow::{CowApiError, CowHost, OrderRejection, order_uid_hex, run};
use shepherd_sdk_test::{MockHost, MockVenue};

const SEPOLIA: u64 = 11_155_111;
Expand Down
5 changes: 3 additions & 2 deletions crates/shepherd-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "CoW-domain guest SDK for Shepherd modules: cow-api host trait, order bridging, revert decoding, and prelude on top of cowprotocol types."
description = "CoW-domain guest SDK for Shepherd modules: cow-api host trait, order bridging, and prelude on top of cowprotocol types."

[lib]
# Plain library - modules link this and emit their own cdylib for the
Expand All @@ -18,10 +18,11 @@ description = "CoW-domain guest SDK for Shepherd modules: cow-api host trait, or
# the table-driven retry classification the cow error surface delegates
# to.
cow-venue = { path = "../cow-venue", features = ["client"] }
# The structured poll seam the keeper run dispatches on.
composable-cow = { path = "../composable-cow" }
nexum-sdk = { path = "../nexum-sdk" }
cowprotocol = { version = "0.2.0", default-features = false }
alloy-primitives.workspace = true
alloy-sol-types.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
Expand Down
18 changes: 8 additions & 10 deletions crates/shepherd-sdk/src/cow/mod.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
//! CoW Protocol bridging.
//!
//! Type conversions and ABI decoding helpers that translate between
//! the on-chain shape (`GPv2OrderData`, `IConditionalOrder` reverts,
//! orderbook JSON) and the typed Rust surface (`OrderData`,
//! `Verdict`, `RetryAction`), plus [`run()`] - the
//! the on-chain shape (`GPv2OrderData`, orderbook JSON) and the typed
//! Rust surface (`OrderData`, `RetryAction`), plus [`run()`] - the
//! poll/submit composition over the keeper stores.
//!
//! The poll seam is the structured [`Verdict`]; the deployed
//! ComposableCoW 1.x reverting wire is decoded behind the quarantined
//! [`LegacyRevertAdapter`].
//! The poll seam is the structured
//! [`Verdict`](composable_cow::Verdict), carried by the
//! `composable-cow` keeper crate together with the quarantined revert
//! decoding; only orderbook concerns live here.
//!
//! The codec submodules stay purely host-neutral: helpers take
//! primitive arguments (`&[u8]`, `Option<&str>`, slices) so they can
//! be unit-tested without wit-bindgen scaffolding and re-used
//! unchanged by TWAP, EthFlow, and future strategy modules. The
//! keeper run is generic over the host traits alone.

pub mod composable;
pub mod error;
pub mod order;
pub mod run;

pub use composable::{IConditionalOrder, LegacyRevertAdapter, Verdict};
pub use error::{
CowApiError, HttpFailure, OrderRejection, RetryAction, classify_api_error,
classify_submit_error, is_already_submitted,
Expand All @@ -33,8 +31,8 @@ pub use run::run;
/// codec, re-exported from the `cow-venue` default slice. The shim keeps
/// this path stable while the module ports move off the legacy surface.
pub use cow_venue::{
BuyToken, BuyTokenDestination, ComposableBody, CowIntent, CowIntentBody, OrderBody,
OrderBuilder, OrderKind, SellToken, SellTokenSource,
BuyToken, BuyTokenDestination, CowIntent, CowIntentBody, OrderBody, OrderBuilder, OrderKind,
SellToken, SellTokenSource,
};

use nexum_sdk::host::Host;
Expand Down
3 changes: 2 additions & 1 deletion crates/shepherd-sdk/src/cow/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
//! the composed behaviour with one capture.

use alloy_primitives::{Address, Bytes};
use composable_cow::Verdict;
use cowprotocol::{GPv2OrderData, OrderCreation, OrderData, Signature};
use nexum_sdk::host::Fault;
use nexum_sdk::keeper::{
ConditionalSource, Gates, Journal, Retrier, RetryAction, Tick, WatchRef, WatchSet,
};

use super::{
CowApiError, CowHost, Verdict, classify_submit_error, gpv2_to_order_data, is_already_submitted,
CowApiError, CowHost, classify_submit_error, gpv2_to_order_data, is_already_submitted,
order_uid_hex,
};

Expand Down
Loading
Loading