diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c74a5d..d82d183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `router-timelock`: `get_op_count` — returns total operations ever queued +- `router-timelock`: `get_ops_by_state(only_pending)` — server-side filtering of ops by state + +### Changed +- `router-multicall`: `call_result` event data now includes the batch caller as the first element: `(caller, target, function, success)`. **Breaking change** — indexers consuming this event must update their schema. + +### Fixed +- `router-timelock`: `queue` now rejects empty descriptions with `InvalidDescription` error +- `router-timelock`: type inference bug in `get_pending_ops` (missing turbofish on `.get()`) + ## [0.1.0] - 2026-03-14 ### Added diff --git a/contracts/router-multicall/src/lib.rs b/contracts/router-multicall/src/lib.rs index 1c474b5..b65a71f 100644 --- a/contracts/router-multicall/src/lib.rs +++ b/contracts/router-multicall/src/lib.rs @@ -207,7 +207,7 @@ impl RouterMulticall { env.events().publish( (Symbol::new(&env, "call_result"),), - (&call.target, &call.function, success), + (&caller, &call.target, &call.function, success), ); } @@ -352,7 +352,7 @@ impl RouterMulticall { mod tests { extern crate std; use super::*; - use soroban_sdk::{testutils::Address as _, Env, Symbol, Vec}; + use soroban_sdk::{testutils::{Address as _, Events}, Env, FromVal, Symbol, Vec}; fn setup() -> (Env, Address, RouterMulticallClient<'static>) { let env = Env::default(); @@ -689,4 +689,39 @@ mod tests { assert_eq!(summary.succeeded, 1); assert_eq!(summary.failed, 1); } + + #[test] + fn test_call_result_event_includes_caller() { + let (env, _admin, client) = setup(); + let mock_id = env.register_contract(None, MockContract); + let caller = Address::generate(&env); + + let mut calls = Vec::new(&env); + calls.push_back(CallDescriptor { + target: mock_id.clone(), + function: Symbol::new(&env, "success"), + required: true, + instruction_budget: None, + }); + + client.execute_batch(&caller, &calls, &false); + + // Find the call_result event — tuple is (contract_id, topics: Vec, data: Val) + let all_events = env.events().all(); + let (_, _, data) = all_events + .iter() + .find(|(_, topics, _)| { + topics + .get(0) + .map(|v| Symbol::from_val(&env, &v) == Symbol::new(&env, "call_result")) + .unwrap_or(false) + }) + .expect("call_result event not found"); + + // Data is a Vec; decode first element as Address and assert it equals caller + let data_vec = soroban_sdk::Vec::::from_val(&env, &data); + let event_caller = Address::from_val(&env, &data_vec.get(0).unwrap()); + + assert_eq!(event_caller, caller); + } } diff --git a/contracts/router-timelock/src/lib.rs b/contracts/router-timelock/src/lib.rs index 7936eff..40104d3 100644 --- a/contracts/router-timelock/src/lib.rs +++ b/contracts/router-timelock/src/lib.rs @@ -68,6 +68,7 @@ pub enum TimelockError { NotCriticalOp = 14, InvalidConfig = 15, InvalidTarget = 16, + InvalidDescription = 17, } // ── Contract ────────────────────────────────────────────────────────────────── @@ -152,6 +153,10 @@ impl RouterTimelock { return Err(TimelockError::InvalidDelay); } + if description.len() == 0 { + return Err(TimelockError::InvalidDescription); + } + let op_id = Self::next_op_id(&env); let eta = env.ledger().timestamp() + delay;