Skip to content

feat(composable): bindings for M2 contract mods (draft, depends on composable-cow PRs) - #7

Draft
brunota20 wants to merge 1 commit into
mainfrom
feat/composable-cow-m2-bindings
Draft

feat(composable): bindings for M2 contract mods (draft, depends on composable-cow PRs)#7
brunota20 wants to merge 1 commit into
mainfrom
feat/composable-cow-m2-bindings

Conversation

@brunota20

@brunota20 brunota20 commented Jun 24, 2026

Copy link
Copy Markdown

Status: Draft. Depends on the four upstream composable-cow proposal PRs landing first. Once those PRs merge into bleu/composable-cow (or upstream cowprotocol/composable-cow), this PR's selectors / event signatures / struct field orders are locked against the canonical Solidity sources via the new tests; no further changes should be needed here.

What this adds

Additive Rust bindings + pure helpers for the four contract changes proposed in the composable-cow grant work. Every addition matches the existing cow-rs pattern: sol!-derived types + pure encode/decode helpers, no Provider plumbing — callers wrap their own RPC layer.

1. ConditionalOrderRegistered event

Added inside the existing ComposableCoW sol! interface block. Indexed owner / handler / ctx (= H(params)) lets watch towers filter at the RPC level with topics: [REGISTERED_HASH, null, handlerAddr]. The existing ConditionalOrderCreated topic-0 is untouched (no indexer breakage).

registered_topic_filter_by_handler(Address) -> [Option<B256>; 4] builds the four-slot filter array a caller passes to eth_subscribe logs.

2. batchGetTradeableOrdersWithSignature

BatchOrderRequest and BatchOrderResult struct bindings + the function selector. BatchOrderResult.order references crate::contracts::GPv2OrderData directly (sol! resolves the type across sibling blocks — confirmed by the existing OrderPlacement event reusing the same type).

decode_batch_order_result / decode_batch_order_results lower each result into a BatchOrderOutcome:

pub enum BatchOrderOutcome {
    Submitted { order: Box<GPv2OrderData>, signature: Bytes },
    PollHint(PollOutcome),
    ComposableCoWError(ComposableCoWError),
    UnknownRevert(Bytes),
}

GPv2OrderData is boxed inside Submitted to keep the enum compact (the order struct alone is ~320 bytes; clippy's large_enum_variant would otherwise fire, and the indirection is invisible to callers via auto-deref).

3. getOrderInfo combined accessor

OrderInfo struct binding (hash, authorized, cabinetValue, swapGuard) + function selector. Mirrors the on-chain helper that bundles four storage reads into one round trip.

4. IConditionalOrder error decoder

The five canonical errors from IConditionalOrder.sol (OrderNotValid, PollTryNextBlock, PollTryAtBlock, PollTryAtEpoch, PollNever) bound in a new sibling sol! interface, plus a decode_conditional_order_revert(&[u8]) -> Option<PollOutcome> decoder.

This is what unlocks TWAP's new behaviour from the polling-hints proposal: PollTryAtEpoch(t0, "before first part"), PollTryAtEpoch(nextPartStart, "between parts"), and PollNever("all parts settled") all decode out of the box. Tests assert each of the five errors round-trips through the decoder, with timestamp / blockNumber / reason arguments preserved byte-exact.

5. ComposableCoW *NotAuthed-style errors

Six errors (ProofNotAuthed, SingleOrderNotAuthed, SwapGuardRestricted, InvalidHandler, InvalidFallbackHandler, InterfaceNotSupported) bound in a sibling ComposableCoWErrors interface + decode_composable_cow_error(&[u8]) -> Option<ComposableCoWError> decoder. Cascades behind decode_conditional_order_revert inside decode_batch_order_result.

Files

  • crates/cowprotocol-primitives/src/composable.rs — extended (events, structs, functions, decoders, helpers, tests)
  • crates/cowprotocol-primitives/src/lib.rs — re-export additions
  • crates/cowprotocol/src/lib.rs — re-export additions

Public surface added (all additive — no existing API changed)

pub enum PollOutcome { NotValid, TryNextBlock, TryAtBlock, TryAtEpoch, Never }
pub enum BatchOrderOutcome { Submitted, PollHint, ComposableCoWError, UnknownRevert }
pub enum ComposableCoWError { ProofNotAuthed, SingleOrderNotAuthed, ... }
pub fn decode_conditional_order_revert(data: &[u8]) -> Option<PollOutcome>
pub fn decode_composable_cow_error(data: &[u8]) -> Option<ComposableCoWError>
pub fn decode_batch_order_result(result: &ComposableCoW::BatchOrderResult) -> BatchOrderOutcome
pub fn decode_batch_order_results(results: &[ComposableCoW::BatchOrderResult]) -> Vec<BatchOrderOutcome>
pub fn registered_topic_filter_by_handler(handler: Address) -> [Option<B256>; 4]
// + sol!-generated types: IConditionalOrder, ComposableCoWErrors,
//   ComposableCoW::ConditionalOrderRegistered, ComposableCoW::BatchOrderRequest,
//   ComposableCoW::BatchOrderResult, ComposableCoW::OrderInfo,
//   ComposableCoW::batchGetTradeableOrdersWithSignatureCall,
//   ComposableCoW::getOrderInfoCall

Tests (16 new + 3 extended cases)

  • conditional_order_registered_round_trips — event topic encoding
  • registered_topic_filter_pins_handler_and_signature — filter helper
  • batch_order_request_round_trips, batch_order_result_round_trips — ABI round-trips
  • decode_batch_order_result_success, _poll_try_at_epoch, _composable_cow_error, _unknown_revert — outcome cascade
  • decode_batch_order_results_preserves_order — order preservation across mixed batch
  • order_info_round_trips, get_order_info_call_round_trips — getOrderInfo combined accessor
  • decode_conditional_order_revert_covers_all_five_errors — all 5 IConditionalOrder errors + the three TWAP reverts byte-exact
  • decode_conditional_order_revert_returns_none_for_unrelated_payloads — safety
  • conditional_order_error_selectors_match_keccak, composable_cow_error_selectors_match_keccak — selector locks
  • decode_composable_cow_error_covers_all_variants
    • 2 new selector cases in composable_cow_selectors_match_keccak (the two new view functions)
    • 1 new topic-hash case in composable_cow_event_topic_hashes_match_keccak

Gates

cargo fmt --all -- --check                                          OK
cargo clippy --all-targets --all-features --workspace -- -Dwarnings OK
cargo test --all-targets --all-features --workspace                 OK (289 passed, 0 failed)
cargo check --target wasm32-unknown-unknown --all-features          OK

Test plan

  • Review the additive sol! surface against the canonical composable-cow PR sources once the upstream PRs settle to confirm field names, argument order, and selector strings.
  • Once the contract PRs merge, retest against a Sepolia deployment with batchGetTradeableOrdersWithSignature returning a mixed batch (one success, one PollTryAtEpoch, one PollNever) and confirm decoding lines up.
  • Confirm the ConditionalOrderRegistered topic filter delivers only the expected handler's events when subscribed via eth_subscribe logs on a live RPC.

@brunota20
brunota20 force-pushed the feat/composable-cow-m2-bindings branch from 281528e to d9c4772 Compare June 24, 2026 18:49
brunota20 added a commit to bleu/nullis-shepherd that referenced this pull request Jun 24, 2026
Integration branch for the four contract surfaces proposed in:
- brunota20/composable-cow#1 (TWAP polling hints)
- brunota20/composable-cow#2 (ConditionalOrderRegistered event)
- brunota20/composable-cow#3 (batchGetTradeableOrdersWithSignature)
- brunota20/composable-cow#4 (getOrderInfo)

Consumes the new bindings from bleu/cow-rs#7
(feat/composable-cow-m2-bindings) via a `[patch.crates-io]` rev
bump from `57f5f55` -> `281528e`.

Changes:
- twap-monitor's per-block poll loop carries a new batched code path
  (`poll_all_watches_batched`) that issues one batched eth_call
  (`ComposableCoW.batchGetTradeableOrdersWithSignature`) instead of N
  per-watch calls. Gated behind a runtime flag
  (`module.toml::[config].use_batch_poll`, default `"false"`); legacy
  per-watch path stays the only runtime code path until the M2
  contracts deploy, so shepherd keeps working against today's
  ComposableCoW.
- The batched path bridges `cowprotocol::PollOutcome` (M2 binding
  shape) to the existing `shepherd_sdk::cow::PollOutcome` lifecycle,
  so TWAP's new precise `PollTryAtEpoch(t0/nextPartStart, ...)` and
  `PollNever("all parts settled")` reverts flow through the same
  `outcome_to_update` -> gate-write dispatch the legacy path uses.
- `ComposableCoWError` (`SingleOrderNotAuthed`, `SwapGuardRestricted`,
  etc.) per-slot reverts drop the watch; `UnknownRevert` is logged at
  Warn and left in place for triage.
- Length-mismatch defence: if the batched response slot count diverges
  from the request count, the whole block is deferred (no per-watch
  state changes) and a Warn fires.
- Seven new MockHost tests cover the batched path: Ready submit,
  `PollTryAtEpoch` -> `next_epoch:` gate, `PollNever` -> drop,
  `SingleOrderNotAuthed` -> drop, length-mismatch defer, runtime-flag
  dispatch routing, and `ConditionalOrderRegistered` indexed-handler
  decode (pins the binding shape for the eventual subscription
  patch).
- `docs/operations/composable-cow-m2-integration.md` captures the
  staged rollout plan, the cleanups that follow Stage 3 (notably
  the COW-1077 `_twap_calldata.py` workaround), and the risks.

Draft: contracts not deployed yet. This branch lands the integration
code so shepherd ships ready-to-flip when the M2 contract mods merge
upstream.

AI Assistance: Claude Code (Opus 4.7) wrote the integration code
against the new cow-rs bindings. A human (Bruno) is accountable.
brunota20 added a commit to bleu/nullis-shepherd that referenced this pull request Jun 25, 2026
Integration branch for the four contract surfaces proposed in:
- brunota20/composable-cow#1 (TWAP polling hints)
- brunota20/composable-cow#2 (ConditionalOrderRegistered event)
- brunota20/composable-cow#3 (batchGetTradeableOrdersWithSignature)
- brunota20/composable-cow#4 (getOrderInfo)

Consumes the new bindings from bleu/cow-rs#7
(feat/composable-cow-m2-bindings) via a `[patch.crates-io]` rev
bump from `57f5f55` -> `281528e`.

Changes:
- twap-monitor's per-block poll loop carries a new batched code path
  (`poll_all_watches_batched`) that issues one batched eth_call
  (`ComposableCoW.batchGetTradeableOrdersWithSignature`) instead of N
  per-watch calls. Gated behind a runtime flag
  (`module.toml::[config].use_batch_poll`, default `"false"`); legacy
  per-watch path stays the only runtime code path until the M2
  contracts deploy, so shepherd keeps working against today's
  ComposableCoW.
- The batched path bridges `cowprotocol::PollOutcome` (M2 binding
  shape) to the existing `shepherd_sdk::cow::PollOutcome` lifecycle,
  so TWAP's new precise `PollTryAtEpoch(t0/nextPartStart, ...)` and
  `PollNever("all parts settled")` reverts flow through the same
  `outcome_to_update` -> gate-write dispatch the legacy path uses.
- `ComposableCoWError` (`SingleOrderNotAuthed`, `SwapGuardRestricted`,
  etc.) per-slot reverts drop the watch; `UnknownRevert` is logged at
  Warn and left in place for triage.
- Length-mismatch defence: if the batched response slot count diverges
  from the request count, the whole block is deferred (no per-watch
  state changes) and a Warn fires.
- Seven new MockHost tests cover the batched path: Ready submit,
  `PollTryAtEpoch` -> `next_epoch:` gate, `PollNever` -> drop,
  `SingleOrderNotAuthed` -> drop, length-mismatch defer, runtime-flag
  dispatch routing, and `ConditionalOrderRegistered` indexed-handler
  decode (pins the binding shape for the eventual subscription
  patch).
- `docs/operations/composable-cow-m2-integration.md` captures the
  staged rollout plan, the cleanups that follow Stage 3 (notably
  the COW-1077 `_twap_calldata.py` workaround), and the risks.

Draft: contracts not deployed yet. This branch lands the integration
code so shepherd ships ready-to-flip when the M2 contract mods merge
upstream.

AI Assistance: Claude Code (Opus 4.7) wrote the integration code
against the new cow-rs bindings. A human (Bruno) is accountable.
Adds Rust bindings + helpers for four contract changes from the M2 grant
deliverable (smart-contract modifications to ComposableCoW + TWAP handler):

- ConditionalOrderRegistered (additive event, indexed handler + ctx).
  ABI bindings + topic filter helper.
- batchGetTradeableOrdersWithSignature view function + BatchOrderRequest /
  BatchOrderResult struct bindings + decoder helpers that lower per-request
  revert payloads into the existing PollOutcome / ComposableCoWError taxonomy.
- getOrderInfo combined accessor + OrderInfo struct binding.
- Decoder extended to ensure TWAP's new precise poll reverts (PollTryAtEpoch
  / PollNever) are recognised by the IConditionalOrder revert taxonomy.

Tests cover ABI round-trip, topic-0 hashes against canonical keccak, the
batch decoder over success / poll-hint / composable error / unknown-revert
paths, and selector locks for the new function calls.
brunota20 added a commit to bleu/nullis-shepherd that referenced this pull request Jun 25, 2026
Integration branch for the four contract surfaces proposed in:
- brunota20/composable-cow#1 (TWAP polling hints)
- brunota20/composable-cow#2 (ConditionalOrderRegistered event)
- brunota20/composable-cow#3 (batchGetTradeableOrdersWithSignature)
- brunota20/composable-cow#4 (getOrderInfo)

Consumes the new bindings from bleu/cow-rs#7
(feat/composable-cow-m2-bindings) via a `[patch.crates-io]` rev
bump from `57f5f55` -> `281528e`.

Changes:
- twap-monitor's per-block poll loop carries a new batched code path
  (`poll_all_watches_batched`) that issues one batched eth_call
  (`ComposableCoW.batchGetTradeableOrdersWithSignature`) instead of N
  per-watch calls. Gated behind a runtime flag
  (`module.toml::[config].use_batch_poll`, default `"false"`); legacy
  per-watch path stays the only runtime code path until the M2
  contracts deploy, so shepherd keeps working against today's
  ComposableCoW.
- The batched path bridges `cowprotocol::PollOutcome` (M2 binding
  shape) to the existing `shepherd_sdk::cow::PollOutcome` lifecycle,
  so TWAP's new precise `PollTryAtEpoch(t0/nextPartStart, ...)` and
  `PollNever("all parts settled")` reverts flow through the same
  `outcome_to_update` -> gate-write dispatch the legacy path uses.
- `ComposableCoWError` (`SingleOrderNotAuthed`, `SwapGuardRestricted`,
  etc.) per-slot reverts drop the watch; `UnknownRevert` is logged at
  Warn and left in place for triage.
- Length-mismatch defence: if the batched response slot count diverges
  from the request count, the whole block is deferred (no per-watch
  state changes) and a Warn fires.
- Seven new MockHost tests cover the batched path: Ready submit,
  `PollTryAtEpoch` -> `next_epoch:` gate, `PollNever` -> drop,
  `SingleOrderNotAuthed` -> drop, length-mismatch defer, runtime-flag
  dispatch routing, and `ConditionalOrderRegistered` indexed-handler
  decode (pins the binding shape for the eventual subscription
  patch).
- `docs/operations/composable-cow-m2-integration.md` captures the
  staged rollout plan, the cleanups that follow Stage 3 (notably
  the COW-1077 `_twap_calldata.py` workaround), and the risks.

Draft: contracts not deployed yet. This branch lands the integration
code so shepherd ships ready-to-flip when the M2 contract mods merge
upstream.
@brunota20
brunota20 force-pushed the feat/composable-cow-m2-bindings branch from d9c4772 to 3ba2075 Compare June 25, 2026 17:48
brunota20 added a commit to bleu/nullis-shepherd that referenced this pull request Jun 26, 2026
Integration branch for the four contract surfaces proposed in:
- brunota20/composable-cow#1 (TWAP polling hints)
- brunota20/composable-cow#2 (ConditionalOrderRegistered event)
- brunota20/composable-cow#3 (batchGetTradeableOrdersWithSignature)
- brunota20/composable-cow#4 (getOrderInfo)

Consumes the new bindings from bleu/cow-rs#7
(feat/composable-cow-m2-bindings) via a `[patch.crates-io]` rev
bump from `57f5f55` -> `281528e`.

Changes:
- twap-monitor's per-block poll loop carries a new batched code path
  (`poll_all_watches_batched`) that issues one batched eth_call
  (`ComposableCoW.batchGetTradeableOrdersWithSignature`) instead of N
  per-watch calls. Gated behind a runtime flag
  (`module.toml::[config].use_batch_poll`, default `"false"`); legacy
  per-watch path stays the only runtime code path until the M2
  contracts deploy, so shepherd keeps working against today's
  ComposableCoW.
- The batched path bridges `cowprotocol::PollOutcome` (M2 binding
  shape) to the existing `shepherd_sdk::cow::PollOutcome` lifecycle,
  so TWAP's new precise `PollTryAtEpoch(t0/nextPartStart, ...)` and
  `PollNever("all parts settled")` reverts flow through the same
  `outcome_to_update` -> gate-write dispatch the legacy path uses.
- `ComposableCoWError` (`SingleOrderNotAuthed`, `SwapGuardRestricted`,
  etc.) per-slot reverts drop the watch; `UnknownRevert` is logged at
  Warn and left in place for triage.
- Length-mismatch defence: if the batched response slot count diverges
  from the request count, the whole block is deferred (no per-watch
  state changes) and a Warn fires.
- Seven new MockHost tests cover the batched path: Ready submit,
  `PollTryAtEpoch` -> `next_epoch:` gate, `PollNever` -> drop,
  `SingleOrderNotAuthed` -> drop, length-mismatch defer, runtime-flag
  dispatch routing, and `ConditionalOrderRegistered` indexed-handler
  decode (pins the binding shape for the eventual subscription
  patch).
- `docs/operations/composable-cow-m2-integration.md` captures the
  staged rollout plan, the cleanups that follow Stage 3 (notably
  the COW-1077 `_twap_calldata.py` workaround), and the risks.

Draft: contracts not deployed yet. This branch lands the integration
code so shepherd ships ready-to-flip when the M2 contract mods merge
upstream.
ribeirojose added a commit that referenced this pull request Jul 1, 2026
Three additive signing-ergonomics gaps, all sharing the same private
machinery so they land as one coherent surface:

- Give `OrderData` the recover counterpart it lacked: `recover_signer`
  and `recover_ecdsa` build the EIP-712 payload internally, so external
  consumers no longer need the doc-hidden `eip712::Order`. The
  orderbook's `verify_owner` now routes through `recover_signer` instead
  of reaching into that module (no behaviour change). (#7)
- Make the forward digest derivation public: `signing_message` is now
  `pub` (and re-exported), with a thin `OrderData::signing_hash`
  wrapper. A new ungated wasm export `ethsign_digest(order_data, chain)`
  returns the exact EIP-191 `personal_sign` bytes for the EthSign
  inject-provider path, completing the "construct, sign externally, lift
  back" story for the one scheme that was missing it. (#9)
- Add named, validating constructors for the on-chain schemes:
  `Signature::eip1271(bytes)` enforces the `EIP1271_MAX_LEN` cap that the
  bare variant skips, and `Signature::pre_sign()` builds the unit
  variant without slice gymnastics. (cowdao-grants#13)

Closes #7
Closes #9
Closes cowdao-grants#13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant