Skip to content

feat(twap-monitor): eth_call poll path + PollOutcome decoder (BLEU-827)#4

Open
brunota20 wants to merge 2 commits into
feat/twap-monitor-indexing-bleu-826from
feat/twap-monitor-polling-bleu-827
Open

feat(twap-monitor): eth_call poll path + PollOutcome decoder (BLEU-827)#4
brunota20 wants to merge 2 commits into
feat/twap-monitor-indexing-bleu-826from
feat/twap-monitor-polling-bleu-827

Conversation

@brunota20

Copy link
Copy Markdown
Collaborator

Summary

`on_event(Event::Block)` walks every persisted watch, skips the ones gated by a future `next_block:` / `next_epoch:` entry, and dispatches the ready ones via `chain::request("eth_call", [{to: COMPOSABLE_COW, data}, "latest"])` to `ComposableCoW.getTradeableOrderWithSignature(owner, params, "", [])`.

  • Successful return → `<(GPv2OrderData, Bytes)>::abi_decode_params` → `PollOutcome::Ready { order, signature }`.
  • Revert payload → `decode_revert` matches the four-byte selector against the five `IConditionalOrder` errors and maps to TryNextBlock / TryOnBlock(n) / TryAtEpoch(t) / DontTryAgain.
  • Anything else (transient RPC error, unmodelled require-revert) falls back to TryNextBlock so the watch is retried instead of dropped.

A local `abi::Params` mirror is needed because sol! cannot cross crate boundaries; the wire format matches `cowprotocol::ConditionalOrderParams` so the selector is byte-equal. `Ready` boxes the order (clippy::large_enum_variant).

Storage conventions (BLEU-830 writes these): `next_block:{owner}:{params_hash}` (u64 LE block number) and `next_epoch:{owner}:{params_hash}` (u64 LE Unix seconds). The watch polls when both pass.

Host follow-up

The chain backend currently swallows alloy's `RpcError::ErrorResp.data` (it becomes the unstructured `host-error.message`). `poll_one` is already wired to consume structured revert hex via `host-error.data` (test `decode_revert_hex_strips_prefix_and_quotes` locks the path). Until the host forwards `error.data`, every revert defaults to TryNextBlock — safe but lossy. Same plumbing also benefits BLEU-829.

Stacks on #3 (BLEU-826 indexing).

Linear: BLEU-827. Ref ADR-0006.

Test plan

  • `cargo test -p twap-monitor` — 17 host tests (3 BLEU-826 regressions + 14 new).
  • `cargo clippy --target wasm32-wasip2 -p twap-monitor -- -Dwarnings`.
  • `cargo clippy -p twap-monitor --tests -- -Dwarnings`.
  • `cargo build --target wasm32-wasip2 --release -p twap-monitor` — 215 KB .wasm (was 96 KB after BLEU-826; serde_json + IConditionalOrder ABI + GPv2OrderData decode linked in).
  • Live eth_call against a Sepolia ComposableCoW deployment — pending BLEU-834 module.toml + host `error.data` forwarding for end-to-end revert classification.

`on_event(Event::Block)` walks every persisted watch, skips the
ones gated by a future `next_block:` / `next_epoch:` entry, and
dispatches the ready ones via `chain::request("eth_call",
[{to: COMPOSABLE_COW, data}, "latest"])` to
`ComposableCoW.getTradeableOrderWithSignature(owner, params,
"", [])`.

Returns:
- Successful return data → `<(GPv2OrderData, Bytes)>::abi_decode_params`
  → `PollOutcome::Ready { order, signature }`.
- Revert payload → `decode_revert` matches the four-byte selector
  against the five `IConditionalOrder` errors:
    OrderNotValid     → DontTryAgain
    PollNever         → DontTryAgain
    PollTryNextBlock  → TryNextBlock
    PollTryAtBlock(n) → TryOnBlock(n)
    PollTryAtEpoch(t) → TryAtEpoch(t)
- Anything else falls back to TryNextBlock so a flaky RPC or
  unmodelled require-revert is retried instead of dropped.

Decoder ABI: a local `abi::Params` struct mirrors the wire format
of `cowprotocol::ConditionalOrderParams` because sol! cannot cross
crate boundaries; the resulting call selector is byte-equal to the
real contract. The successful return path decodes into the
canonical `cowprotocol::GPv2OrderData` directly, so the 12-field
struct is not duplicated. `Ready` boxes the order to keep
`PollOutcome` cache-friendly (clippy::large_enum_variant).

Storage conventions (shared with BLEU-830, which writes these):
- `next_block:{owner}:{params_hash}` -> u64 LE — block number gate
- `next_epoch:{owner}:{params_hash}` -> u64 LE — Unix-seconds gate
Either / both / neither may be set; the watch polls when both pass.
`block.timestamp` is milliseconds per WIT, so we divide by 1000 to
compare against the `TryAtEpoch` (seconds) convention.

Host follow-up: the chain backend currently swallows alloy's
`RpcError::ErrorResp.data` (it becomes `host-error.message`,
unstructured). `poll_one` is wired to consume structured revert
hex via `host-error.data` once that lands — the `decode_revert_hex`
test locks the path. Until then, every revert defaults to
TryNextBlock, which is the safe choice.

Tests: 14 new (return round-trip, all five revert variants, hex
plumbing, eth_call JSON shape, watch-key round-trip, U256
saturation), keeping the 3 BLEU-826 regressions. `.wasm` grows
from 96 KB to 215 KB (serde_json + IConditionalOrder ABI + the
GPv2OrderData decode path linked in).

Linear: BLEU-827. Ref ADR-0006.
@linear-code

linear-code Bot commented Jun 15, 2026

Copy link
Copy Markdown

BLEU-827

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