diff --git a/src/many-ledger/src/error.rs b/src/many-ledger/src/error.rs index 91938d8a..a5c7b517 100644 --- a/src/many-ledger/src/error.rs +++ b/src/many-ledger/src/error.rs @@ -20,6 +20,12 @@ define_attribute_many_error!( } ); +define_attribute_many_error!( + attribute 4 => { + 1: pub fn event_not_found(id) => "Event not found: {id}.", + } +); + define_attribute_many_error!( attribute 11 => { 1: pub fn token_info_not_found(symbol) => "Token information not found in persistent storage: {symbol}.", diff --git a/src/many-ledger/src/module/event.rs b/src/many-ledger/src/module/event.rs index f91954e4..ced308e0 100644 --- a/src/many-ledger/src/module/event.rs +++ b/src/many-ledger/src/module/event.rs @@ -7,7 +7,8 @@ use many_modules::events::{ EventFilterAttributeSpecific, EventFilterAttributeSpecificIndex, EventInfo, EventLog, }; use many_types::{CborRange, Timestamp, VecOrSingle}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, Bound}; +use std::ops::RangeBounds; const MAXIMUM_EVENT_COUNT: usize = 100; @@ -121,10 +122,24 @@ impl events::EventsModuleBackend for LedgerModuleImpl { let storage = &self.storage; let nb_events = storage.nb_events()?; - let iter = storage.iter_events( - filter.id_range.unwrap_or_default(), - order.unwrap_or_default(), - ); + + // Check that the id range is valid. + // Return an error if any key doesn't exist + let id_range = filter.id_range.unwrap_or_default(); + match id_range.start_bound() { + Bound::Included(x) | Bound::Excluded(x) => { + storage.check_event_id(x)?; + } + Bound::Unbounded => {} + } + match id_range.end_bound() { + Bound::Included(x) | Bound::Excluded(x) => { + storage.check_event_id(x)?; + } + Bound::Unbounded => {} + } + + let iter = storage.iter_events(id_range, order.unwrap_or_default()); let iter = Box::new(iter.map(|item| { let (_k, v) = item.map_err(ManyError::unknown)?; diff --git a/src/many-ledger/src/storage/event.rs b/src/many-ledger/src/storage/event.rs index ef7f8116..957eb52e 100644 --- a/src/many-ledger/src/storage/event.rs +++ b/src/many-ledger/src/storage/event.rs @@ -38,6 +38,17 @@ impl LedgerStorage { self.latest_tid.clone() } + pub(crate) fn check_event_id(&self, id: &events::EventId) -> Result<(), ManyError> { + match self + .persistent_store + .get(&key_for_event(id.clone())) + .map_err(error::storage_get_failed)? + { + None => Err(error::event_not_found(hex::encode(id.as_ref()))), + Some(_) => Ok(()), + } + } + pub fn nb_events(&self) -> Result { self.persistent_store .get(EVENT_COUNT_ROOT) diff --git a/src/many-ledger/tests/events.rs b/src/many-ledger/tests/events.rs index ac36fe23..eb6a3410 100644 --- a/src/many-ledger/tests/events.rs +++ b/src/many-ledger/tests/events.rs @@ -6,7 +6,8 @@ use many_modules::account::features::multisig::{ self, AccountMultisigModuleBackend, MultisigTransactionState, }; use many_modules::events::{ - self, EventFilterAttributeSpecific, EventFilterAttributeSpecificIndex, EventsModuleBackend, + self, EventFilterAttributeSpecific, EventFilterAttributeSpecificIndex, EventId, + EventsModuleBackend, }; use many_modules::ledger; use many_modules::ledger::LedgerCommandsModuleBackend; @@ -228,6 +229,197 @@ fn list_filter_kind() { assert!(list_return.events[0].is_about(id)); } +#[test] +fn list_filter_id() { + let Setup { + mut module_impl, + id, + .. + } = setup(); + + // Create 5 events + for i in 0..5 { + send(&mut module_impl, id, identity(i)); + } + + // Collect all events ids + let events_ids = get_all_events_ids(&mut module_impl); + + let tests = vec![ + // Unbounded bounds + (Bound::Unbounded, Bound::Unbounded, 5, 0..5), + // Included bounds + ( + Bound::Unbounded, + Bound::Included(events_ids[3].clone()), + 4, + 0..4, + ), + ( + Bound::Included(events_ids[1].clone()), + Bound::Unbounded, + 4, + 1..5, + ), + ( + Bound::Included(events_ids[1].clone()), + Bound::Included(events_ids[3].clone()), + 3, + 1..4, + ), + // Excluded bounds + ( + Bound::Unbounded, + Bound::Excluded(events_ids[4].clone()), + 4, + 0..4, + ), + ( + Bound::Excluded(events_ids[0].clone()), + Bound::Unbounded, + 4, + 1..5, + ), + ( + Bound::Excluded(events_ids[0].clone()), + Bound::Excluded(events_ids[4].clone()), + 3, + 1..4, + ), + // Mixed Included/Excluded bounds + ( + Bound::Included(events_ids[1].clone()), + Bound::Excluded(events_ids[4].clone()), + 3, + 1..4, + ), + ( + Bound::Excluded(events_ids[0].clone()), + Bound::Included(events_ids[3].clone()), + 3, + 1..4, + ), + ]; + + for (start_bound, end_bound, expected_len, id_range) in tests { + let list_return = filter_events(&mut module_impl, start_bound, end_bound); + assert_eq!(list_return.nb_events, 5); + assert_eq!(list_return.events.len(), expected_len); + + list_return + .events + .into_iter() + .zip(events_ids[id_range].iter()) + .for_each(|(event, id)| { + assert_eq!(event.id, *id); + }); + } +} + +fn get_all_events_ids(module_impl: &mut LedgerModuleImpl) -> Vec { + let result = module_impl + .list(events::ListArgs { + count: None, + order: None, + filter: None, + }) + .unwrap(); + + assert_eq!(result.nb_events, 5); + assert_eq!(result.events.len(), 5); + + result.events.iter().map(|e| e.id.clone()).collect() +} + +fn filter_events( + module_impl: &mut LedgerModuleImpl, + start_bound: Bound, + end_bound: Bound, +) -> events::ListReturns { + let result = module_impl.list(events::ListArgs { + count: None, + order: None, + filter: Some(events::EventFilter { + id_range: Some(CborRange { + start: start_bound, + end: end_bound, + }), + ..events::EventFilter::default() + }), + }); + + assert!(result.is_ok()); + + result.unwrap() +} + +#[test] +fn list_invalid_filter_id() { + let Setup { + mut module_impl, + id, + .. + } = setup(); + + // Create 5 events + for i in 0..5 { + send(&mut module_impl, id, identity(i)); + } + let invalid_tests = vec![ + // Hypothetical error case - included range out of bounds + ( + Bound::Included(vec![6, 7, 8].into()), + Bound::Included(vec![9, 10, 11].into()), + ), + // Hypothetical error case - excluded range out of bounds + ( + Bound::Excluded(vec![6, 7, 8].into()), + Bound::Excluded(vec![9, 10, 11].into()), + ), + // Hypothetical error case - start included and end excluded, but range is out of bounds + ( + Bound::Included(vec![0].into()), + Bound::Excluded(vec![10].into()), + ), + // Hypothetical error case - start excluded and end included, but range is out of bounds + ( + Bound::Excluded(vec![0].into()), + Bound::Included(vec![10].into()), + ), + // Hypothetical error case - start unbounded and end included, but range is out of bounds + (Bound::Unbounded, Bound::Included(vec![6].into())), + // Hypothetical error case - start included and end unbounded, but range is out of bounds + (Bound::Included(vec![0].into()), Bound::Unbounded), + // Hypothetical error case - start unbounded and end excluded, but range is out of bounds + (Bound::Unbounded, Bound::Excluded(vec![6].into())), + // Hypothetical error case - start excluded and end unbounded, but range is out of bounds + (Bound::Excluded(vec![0].into()), Bound::Unbounded), + ]; + + for (start_bound, end_bound) in invalid_tests { + assert_invalid_filter(&mut module_impl, start_bound, end_bound); + } +} + +fn assert_invalid_filter( + module_impl: &mut LedgerModuleImpl, + start_bound: Bound, + end_bound: Bound, +) { + let result = module_impl.list(events::ListArgs { + count: None, + order: None, + filter: Some(events::EventFilter { + id_range: Some(CborRange { + start: start_bound, + end: end_bound, + }), + ..events::EventFilter::default() + }), + }); + + assert!(result.is_err()); +} #[test] fn list_filter_date() { let Setup { diff --git a/src/many-modules/src/_4_events/list.rs b/src/many-modules/src/_4_events/list.rs index 4be4a7e1..72e51b16 100644 --- a/src/many-modules/src/_4_events/list.rs +++ b/src/many-modules/src/_4_events/list.rs @@ -15,7 +15,7 @@ pub struct ListArgs { pub filter: Option, } -#[derive(Encode, Decode)] +#[derive(Debug, Encode, Decode)] #[cbor(map)] pub struct ListReturns { #[n(0)]