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-registry/src/lib.rs b/contracts/router-registry/src/lib.rs index 1979419..56626e8 100644 --- a/contracts/router-registry/src/lib.rs +++ b/contracts/router-registry/src/lib.rs @@ -57,6 +57,7 @@ pub enum RegistryError { InvalidVersion = 7, VersionNotFound = 8, InvalidConstraint = 9, + AllVersionsDeprecated = 10, } // ── Contract ────────────────────────────────────────────────────────────────── @@ -213,6 +214,9 @@ impl RouterRegistry { /// * [`RegistryError::NotFound`] — if no non-deprecated entry exists for `name`. pub fn get_latest(env: Env, name: String) -> Result { let versions = Self::get_versions_list(&env, &name); + if versions.is_empty() { + return Err(RegistryError::NotFound); + } // Iterate in reverse to find latest non-deprecated let len = versions.len(); let mut i = len; @@ -228,7 +232,7 @@ impl RouterRegistry { return Ok(entry); } } - Err(RegistryError::NotFound) + Err(RegistryError::AllVersionsDeprecated) } /// Get the latest non-deprecated entry matching a semver constraint. @@ -270,7 +274,7 @@ impl RouterRegistry { return Ok(entry); } } - return Err(RegistryError::NotFound); + return Err(RegistryError::AllVersionsDeprecated); } let constraint_str = constraint.unwrap(); @@ -647,7 +651,7 @@ mod tests { client.register(&admin, &name, &addr, &1); client.deprecate(&admin, &name, &1); let result = client.try_get_latest(&name); - assert_eq!(result, Err(Ok(RegistryError::NotFound))); + assert_eq!(result, Err(Ok(RegistryError::AllVersionsDeprecated))); } #[test] @@ -793,7 +797,7 @@ mod tests { // When all versions are deprecated, get_latest should return NotFound let result = client.try_get_latest(&name); - assert_eq!(result, Err(Ok(RegistryError::NotFound))); + assert_eq!(result, Err(Ok(RegistryError::AllVersionsDeprecated))); } #[test] @@ -876,7 +880,7 @@ mod tests { let constraint = String::from_str(&env, "2"); let result = client.try_get_latest_with_constraint(&name, &Some(constraint)); assert!(result.is_ok()); - let entry = result.unwrap(); + let entry = result.unwrap().unwrap(); assert_eq!(entry.version, 2); } @@ -896,7 +900,7 @@ mod tests { let constraint = String::from_str(&env, ">=2"); let result = client.try_get_latest_with_constraint(&name, &Some(constraint)); assert!(result.is_ok()); - let entry = result.unwrap(); + let entry = result.unwrap().unwrap(); assert_eq!(entry.version, 3); } @@ -916,7 +920,7 @@ mod tests { let constraint = String::from_str(&env, "<3"); let result = client.try_get_latest_with_constraint(&name, &Some(constraint)); assert!(result.is_ok()); - let entry = result.unwrap(); + let entry = result.unwrap().unwrap(); assert_eq!(entry.version, 2); } @@ -936,7 +940,7 @@ mod tests { let constraint = String::from_str(&env, "^2"); let result = client.try_get_latest_with_constraint(&name, &Some(constraint)); assert!(result.is_ok()); - let entry = result.unwrap(); + let entry = result.unwrap().unwrap(); assert_eq!(entry.version, 2); } @@ -970,7 +974,7 @@ mod tests { let constraint = String::from_str(&env, ">=2"); let result = client.try_get_latest_with_constraint(&name, &Some(constraint)); assert!(result.is_ok()); - let entry = result.unwrap(); + let entry = result.unwrap().unwrap(); assert_eq!(entry.version, 2); } 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;