From 049b1843057473a73adda358d47af781595731bc Mon Sep 17 00:00:00 2001 From: brunota20 Date: Mon, 15 Jun 2026 09:48:50 -0300 Subject: [PATCH] =?UTF-8?q?feat(twap-monitor):=20index=20ConditionalOrderC?= =?UTF-8?q?reated=20=E2=86=92=20local-store=20(BLEU-826)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `on_event(Event::Logs)` decodes each log against `ComposableCoW.ConditionalOrderCreated` via `alloy_sol_types`, extracts `(owner, params)`, and writes `watch:{owner}:{params_hash}` to local-store with the abi-encoded `ConditionalOrderParams` as the value. BLEU-827 reads this back via `list-keys("watch:")` and the value is exactly the `(handler, salt, staticInput)` tuple the poll path passes to `getTradeableOrderWithSignature`. Idempotency: `local_store::set` overwrites in place, so re-org replay or overlapping subscription windows produce no observable side effect. Resilience: `decode_conditional_order_created` returns `None` when topic0 does not match the event signature or the payload fails ABI decoding. Adjacent events on the same subscription (MerkleRootSet, SwapGuardSet) are silently skipped instead of short-circuiting the batch. The fn is on plain slices so the host-free unit tests cover well-formed / wrong-topic / empty- topics without wit-bindgen scaffolding. Block, Tick, and Message variants of `Event` are left unhandled in this PR — `Event::Block` dispatch lands in BLEU-827 (poll path); the other two are not used by this module. Adds `alloy-primitives` as a direct dep so the topic/data plumbing does not rely on alloy types leaking through `cowprotocol`'s re-exports. `cargo build --target wasm32-wasip2 --release -p twap-monitor` emits a 96 KB .wasm (up from the 65 KB skeleton because of the alloy + cowprotocol composable types now linked in). --- modules/twap-monitor/Cargo.toml | 3 +- modules/twap-monitor/src/lib.rs | 99 +++++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/modules/twap-monitor/Cargo.toml b/modules/twap-monitor/Cargo.toml index bafc696..6cde093 100644 --- a/modules/twap-monitor/Cargo.toml +++ b/modules/twap-monitor/Cargo.toml @@ -10,5 +10,6 @@ crate-type = ["cdylib"] [dependencies] cowprotocol = { version = "1.0.0-alpha.3", default-features = false } -alloy-sol-types = { version = "1.5", default-features = false } +alloy-primitives = { version = "1.5", default-features = false, features = ["std"] } +alloy-sol-types = { version = "1.5", default-features = false, features = ["std"] } wit-bindgen = { version = "0.57", default-features = false, features = ["macros", "realloc"] } diff --git a/modules/twap-monitor/src/lib.rs b/modules/twap-monitor/src/lib.rs index 72c0763..fbf0515 100644 --- a/modules/twap-monitor/src/lib.rs +++ b/modules/twap-monitor/src/lib.rs @@ -8,8 +8,10 @@ wit_bindgen::generate!({ generate_all, }); -use nexum::host::logging; -use nexum::host::types; +use alloy_primitives::{Address, B256, keccak256}; +use alloy_sol_types::{SolEvent, SolValue}; +use cowprotocol::{ComposableCoW::ConditionalOrderCreated, ConditionalOrderParams}; +use nexum::host::{local_store, logging, types}; struct TwapMonitor; @@ -19,11 +21,98 @@ impl Guest for TwapMonitor { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), HostError> { - // Dispatch on Event::Log (ConditionalOrderCreated) and Event::Block - // (TWAP poll tick) lands in BLEU-826 / BLEU-827. + fn on_event(event: types::Event) -> Result<(), HostError> { + if let types::Event::Logs(logs) = event { + for log in &logs { + if let Some((owner, params)) = + decode_conditional_order_created(&log.topics, &log.data) + { + persist_watch(owner, ¶ms)?; + } + } + } + // Event::Block (TWAP poll) lands in BLEU-827; Tick / Message are not + // used by this module. Ok(()) } } +/// Decode a raw event log against `ComposableCoW.ConditionalOrderCreated`. +/// +/// Returns `None` when topic0 does not match the event signature or the +/// payload fails ABI decoding — both are non-fatal for an indexer that +/// shares a subscription with adjacent events. Kept on plain slices so +/// the host-free unit tests under `#[cfg(test)]` can call it without +/// wit-bindgen scaffolding. +fn decode_conditional_order_created( + topics: &[Vec], + data: &[u8], +) -> Option<(Address, ConditionalOrderParams)> { + let topic0 = topics.first()?; + if topic0.len() != 32 || B256::from_slice(topic0) != ConditionalOrderCreated::SIGNATURE_HASH { + return None; + } + let words: Vec = topics + .iter() + .filter(|t| t.len() == 32) + .map(|t| B256::from_slice(t)) + .collect(); + let decoded = ConditionalOrderCreated::decode_raw_log(words, data).ok()?; + Some((decoded.owner, decoded.params)) +} + +/// Persist a watch entry. `set` overwrites in place, so re-indexing the +/// same log (re-org replay, overlapping subscription windows) produces no +/// observable side effect — the idempotency the issue asks for. +fn persist_watch(owner: Address, params: &ConditionalOrderParams) -> Result<(), HostError> { + let encoded = params.abi_encode(); + let params_hash = keccak256(&encoded); + let key = format!("watch:{owner:#x}:{params_hash:#x}"); + local_store::set(&key, &encoded)?; + logging::log(logging::Level::Info, &format!("indexed {key}")); + Ok(()) +} + export!(TwapMonitor); + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, b256, hex}; + + #[test] + fn decodes_well_formed_log() { + let owner = address!("00112233445566778899aabbccddeeff00112233"); + let params = ConditionalOrderParams { + handler: address!("ffeeddccbbaa00998877665544332211ffeeddcc"), + salt: b256!("0101010101010101010101010101010101010101010101010101010101010101"), + staticInput: hex!("deadbeef").to_vec().into(), + }; + // address indexed: 20-byte address left-padded to 32 bytes. + let owner_topic = { + let mut t = vec![0u8; 12]; + t.extend_from_slice(owner.as_slice()); + t + }; + let topics = vec![ConditionalOrderCreated::SIGNATURE_HASH.to_vec(), owner_topic]; + let data = params.abi_encode(); + + let (decoded_owner, decoded_params) = + decode_conditional_order_created(&topics, &data).expect("decode succeeds"); + assert_eq!(decoded_owner, owner); + assert_eq!(decoded_params, params); + } + + #[test] + fn rejects_wrong_topic() { + let topics = + vec![b256!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").to_vec()]; + let data = vec![]; + assert!(decode_conditional_order_created(&topics, &data).is_none()); + } + + #[test] + fn rejects_empty_topics() { + assert!(decode_conditional_order_created(&[], &[]).is_none()); + } +}