// This file is part of Substrate. // Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Scheduler //! A Pallet for scheduling dispatches. //! //! - [`Config`] //! - [`Call`] //! - [`Pallet`] //! //! ## Overview //! //! This Pallet exposes capabilities for scheduling dispatches to occur at a //! specified block number or at a specified period. These scheduled dispatches //! may be named or anonymous and may be canceled. //! //! **NOTE:** The scheduled calls will be dispatched with the default filter //! for the origin: namely `frame_system::Config::BaseCallFilter` for all origin //! except root which will get no filter. And not the filter contained in origin //! use to call `fn schedule`. //! //! If a call is scheduled using proxy or whatever mecanism which adds filter, //! then those filter will not be used when dispatching the schedule call. //! //! ## Interface //! //! ### Dispatchable Functions //! //! * `schedule` - schedule a dispatch, which may be periodic, to occur at a specified block and //! with a specified priority. //! * `cancel` - cancel a scheduled dispatch, specified by block number and index. //! * `schedule_named` - augments the `schedule` interface with an additional `Vec` parameter //! that can be used for identification. //! * `cancel_named` - the named complement to the cancel function. // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; pub mod weights; use codec::{Codec, Decode, Encode}; use frame_support::{ dispatch::{DispatchError, DispatchResult, Dispatchable, Parameter}, traits::{ schedule::{self, DispatchTime}, EnsureOrigin, Get, IsType, OriginTrait, }, weights::{GetDispatchInfo, Weight}, }; use frame_system::{self as system, ensure_signed}; pub use pallet::*; use scale_info::TypeInfo; use sp_runtime::{ traits::{BadOrigin, One, Saturating, Zero}, RuntimeDebug, }; use sp_std::{borrow::Borrow, marker::PhantomData, prelude::*}; pub use weights::WeightInfo; /// Just a simple index for naming period tasks. pub type PeriodicIndex = u32; /// The location of a scheduled task that can be used to remove it. pub type TaskAddress = (BlockNumber, u32); #[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] #[derive(Clone, RuntimeDebug, Encode, Decode)] struct ScheduledV1 { maybe_id: Option>, priority: schedule::Priority, call: Call, maybe_periodic: Option>, } /// Information regarding an item to be executed in the future. #[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] #[derive(Clone, RuntimeDebug, Encode, Decode, TypeInfo)] pub struct ScheduledV2 { /// The unique identity for this task, if there is one. maybe_id: Option>, /// This task's priority. priority: schedule::Priority, /// The call to be dispatched. call: Call, /// If the call is periodic, then this points to the information concerning that. maybe_periodic: Option>, /// The origin to dispatch the call. origin: PalletsOrigin, _phantom: PhantomData, } /// The current version of Scheduled struct. pub type Scheduled = ScheduledV2; // A value placed in storage that represents the current version of the Scheduler storage. // This value is used by the `on_runtime_upgrade` logic to determine whether we run // storage migration logic. #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)] enum Releases { V1, V2, } impl Default for Releases { fn default() -> Self { Releases::V1 } } #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); /// `system::Config` should always be included in our implied traits. #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. type Event: From> + IsType<::Event>; /// The aggregated origin which the dispatch will take. type Origin: OriginTrait + From + IsType<::Origin>; /// The caller origin, overarching type of all pallets origins. type PalletsOrigin: From> + Codec + Clone + Eq + TypeInfo; /// The aggregated call type. type Call: Parameter + Dispatchable::Origin> + GetDispatchInfo + From>; /// The maximum weight that may be scheduled per block for any dispatchables of less /// priority than `schedule::HARD_DEADLINE`. #[pallet::constant] type MaximumWeight: Get; /// Required origin to schedule or cancel calls. type ScheduleOrigin: EnsureOrigin<::Origin>; /// The maximum number of scheduled calls in the queue for a single block. /// Not strictly enforced, but used for weight estimation. #[pallet::constant] type MaxScheduledPerBlock: Get; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } /// Items to be executed, indexed by the block number that they should be executed on. #[pallet::storage] pub type Agenda = StorageMap< _, Twox64Concat, T::BlockNumber, Vec::Call, T::BlockNumber, T::PalletsOrigin, T::AccountId>>>, ValueQuery, >; /// Lookup from identity to the block number and index of the task. #[pallet::storage] pub(crate) type Lookup = StorageMap<_, Twox64Concat, Vec, TaskAddress>; /// Storage version of the pallet. /// /// New networks start with last version. #[pallet::storage] pub(crate) type StorageVersion = StorageValue<_, Releases, ValueQuery>; /// Events type. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Scheduled some task. \[when, index\] Scheduled(T::BlockNumber, u32), /// Canceled some task. \[when, index\] Canceled(T::BlockNumber, u32), /// Dispatched some task. \[task, id, result\] Dispatched(TaskAddress, Option>, DispatchResult), } #[pallet::error] pub enum Error { /// Failed to schedule a call FailedToSchedule, /// Cannot find the scheduled call. NotFound, /// Given target block number is in the past. TargetBlockNumberInPast, /// Reschedule failed because it does not change scheduled time. RescheduleNoChange, } #[pallet::genesis_config] pub struct GenesisConfig; #[cfg(feature = "std")] impl Default for GenesisConfig { fn default() -> Self { Self } } #[pallet::genesis_build] impl GenesisBuild for GenesisConfig { fn build(&self) { StorageVersion::::put(Releases::V2); } } #[pallet::hooks] impl Hooks> for Pallet { /// Execute the scheduled calls /// /// # /// - S = Number of already scheduled calls /// - N = Named scheduled calls /// - P = Periodic Calls /// - Base Weight: 9.243 + 23.45 * S µs /// - DB Weight: /// - Read: Agenda + Lookup * N + Agenda(Future) * P /// - Write: Agenda + Lookup * N + Agenda(future) * P /// # fn on_initialize(now: T::BlockNumber) -> Weight { let limit = T::MaximumWeight::get(); let mut queued = Agenda::::take(now) .into_iter() .enumerate() .filter_map(|(index, s)| s.map(|inner| (index as u32, inner))) .collect::>(); if queued.len() as u32 > T::MaxScheduledPerBlock::get() { log::warn!( target: "runtime::scheduler", "Warning: This block has more items queued in Scheduler than \ expected from the runtime configuration. An update might be needed." ); } queued.sort_by_key(|(_, s)| s.priority); let base_weight: Weight = T::DbWeight::get().reads_writes(1, 2); // Agenda + Agenda(next) let mut total_weight: Weight = 0; queued .into_iter() .enumerate() .scan(base_weight, |cumulative_weight, (order, (index, s))| { *cumulative_weight = cumulative_weight.saturating_add(s.call.get_dispatch_info().weight); let origin = <::Origin as From>::from(s.origin.clone()) .into(); if ensure_signed(origin).is_ok() { // AccountData for inner call origin accountdata. *cumulative_weight = cumulative_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); } if s.maybe_id.is_some() { // Remove/Modify Lookup *cumulative_weight = cumulative_weight.saturating_add(T::DbWeight::get().writes(1)); } if s.maybe_periodic.is_some() { // Read/Write Agenda for future block *cumulative_weight = cumulative_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); } Some((order, index, *cumulative_weight, s)) }) .filter_map(|(order, index, cumulative_weight, mut s)| { // We allow a scheduled call if any is true: // - It's priority is `HARD_DEADLINE` // - It does not push the weight past the limit. // - It is the first item in the schedule if s.priority <= schedule::HARD_DEADLINE || cumulative_weight <= limit || order == 0 { let r = s.call.clone().dispatch(s.origin.clone().into()); let maybe_id = s.maybe_id.clone(); if let &Some((period, count)) = &s.maybe_periodic { if count > 1 { s.maybe_periodic = Some((period, count - 1)); } else { s.maybe_periodic = None; } let next = now + period; // If scheduled is named, place it's information in `Lookup` if let Some(ref id) = s.maybe_id { let next_index = Agenda::::decode_len(now + period).unwrap_or(0); Lookup::::insert(id, (next, next_index as u32)); } Agenda::::append(next, Some(s)); } else { if let Some(ref id) = s.maybe_id { Lookup::::remove(id); } } Self::deposit_event(Event::Dispatched( (now, index), maybe_id, r.map(|_| ()).map_err(|e| e.error), )); total_weight = cumulative_weight; None } else { Some(Some(s)) } }) .for_each(|unused| { let next = now + One::one(); Agenda::::append(next, unused); }); total_weight } } #[pallet::call] impl Pallet { /// Anonymously schedule a task. /// /// # /// - S = Number of already scheduled calls /// - Base Weight: 22.29 + .126 * S µs /// - DB Weight: /// - Read: Agenda /// - Write: Agenda /// - Will use base weight of 25 which should be good for up to 30 scheduled calls /// # #[pallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] pub fn schedule( origin: OriginFor, when: T::BlockNumber, maybe_periodic: Option>, priority: schedule::Priority, call: Box<::Call>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_schedule( DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), *call, )?; Ok(()) } /// Cancel an anonymously scheduled task. /// /// # /// - S = Number of already scheduled calls /// - Base Weight: 22.15 + 2.869 * S µs /// - DB Weight: /// - Read: Agenda /// - Write: Agenda, Lookup /// - Will use base weight of 100 which should be good for up to 30 scheduled calls /// # #[pallet::weight(::WeightInfo::cancel(T::MaxScheduledPerBlock::get()))] pub fn cancel(origin: OriginFor, when: T::BlockNumber, index: u32) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_cancel(Some(origin.caller().clone()), (when, index))?; Ok(()) } /// Schedule a named task. /// /// # /// - S = Number of already scheduled calls /// - Base Weight: 29.6 + .159 * S µs /// - DB Weight: /// - Read: Agenda, Lookup /// - Write: Agenda, Lookup /// - Will use base weight of 35 which should be good for more than 30 scheduled calls /// # #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] pub fn schedule_named( origin: OriginFor, id: Vec, when: T::BlockNumber, maybe_periodic: Option>, priority: schedule::Priority, call: Box<::Call>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_schedule_named( id, DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), *call, )?; Ok(()) } /// Cancel a named scheduled task. /// /// # /// - S = Number of already scheduled calls /// - Base Weight: 24.91 + 2.907 * S µs /// - DB Weight: /// - Read: Agenda, Lookup /// - Write: Agenda, Lookup /// - Will use base weight of 100 which should be good for up to 30 scheduled calls /// # #[pallet::weight(::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get()))] pub fn cancel_named(origin: OriginFor, id: Vec) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_cancel_named(Some(origin.caller().clone()), id)?; Ok(()) } /// Anonymously schedule a task after a delay. /// /// # /// Same as [`schedule`]. /// # #[pallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] pub fn schedule_after( origin: OriginFor, after: T::BlockNumber, maybe_periodic: Option>, priority: schedule::Priority, call: Box<::Call>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_schedule( DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), *call, )?; Ok(()) } /// Schedule a named task after a delay. /// /// # /// Same as [`schedule_named`](Self::schedule_named). /// # #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] pub fn schedule_named_after( origin: OriginFor, id: Vec, after: T::BlockNumber, maybe_periodic: Option>, priority: schedule::Priority, call: Box<::Call>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_schedule_named( id, DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), *call, )?; Ok(()) } } } impl Pallet { /// Migrate storage format from V1 to V2. /// Return true if migration is performed. pub fn migrate_v1_to_t2() -> bool { if StorageVersion::::get() == Releases::V1 { StorageVersion::::put(Releases::V2); Agenda::::translate::< Vec::Call, T::BlockNumber>>>, _, >(|_, agenda| { Some( agenda .into_iter() .map(|schedule| { schedule.map(|schedule| ScheduledV2 { maybe_id: schedule.maybe_id, priority: schedule.priority, call: schedule.call, maybe_periodic: schedule.maybe_periodic, origin: system::RawOrigin::Root.into(), _phantom: Default::default(), }) }) .collect::>(), ) }); true } else { false } } /// Helper to migrate scheduler when the pallet origin type has changed. pub fn migrate_origin + codec::Decode>() { Agenda::::translate::< Vec::Call, T::BlockNumber, OldOrigin, T::AccountId>>>, _, >(|_, agenda| { Some( agenda .into_iter() .map(|schedule| { schedule.map(|schedule| Scheduled { maybe_id: schedule.maybe_id, priority: schedule.priority, call: schedule.call, maybe_periodic: schedule.maybe_periodic, origin: schedule.origin.into(), _phantom: Default::default(), }) }) .collect::>(), ) }); } fn resolve_time(when: DispatchTime) -> Result { let now = frame_system::Pallet::::block_number(); let when = match when { DispatchTime::At(x) => x, // The current block has already completed it's scheduled tasks, so // Schedule the task at lest one block after this current block. DispatchTime::After(x) => now.saturating_add(x).saturating_add(One::one()), }; if when <= now { return Err(Error::::TargetBlockNumberInPast.into()) } Ok(when) } fn do_schedule( when: DispatchTime, maybe_periodic: Option>, priority: schedule::Priority, origin: T::PalletsOrigin, call: ::Call, ) -> Result, DispatchError> { let when = Self::resolve_time(when)?; // sanitize maybe_periodic let maybe_periodic = maybe_periodic .filter(|p| p.1 > 1 && !p.0.is_zero()) // Remove one from the number of repetitions since we will schedule one now. .map(|(p, c)| (p, c - 1)); let s = Some(Scheduled { maybe_id: None, priority, call, maybe_periodic, origin, _phantom: PhantomData::::default(), }); Agenda::::append(when, s); let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; if index > T::MaxScheduledPerBlock::get() { log::warn!( target: "runtime::scheduler", "Warning: There are more items queued in the Scheduler than \ expected from the runtime configuration. An update might be needed.", ); } Self::deposit_event(Event::Scheduled(when, index)); Ok((when, index)) } fn do_cancel( origin: Option, (when, index): TaskAddress, ) -> Result<(), DispatchError> { let scheduled = Agenda::::try_mutate(when, |agenda| { agenda.get_mut(index as usize).map_or( Ok(None), |s| -> Result>, DispatchError> { if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) { if *o != s.origin { return Err(BadOrigin.into()) } }; Ok(s.take()) }, ) })?; if let Some(s) = scheduled { if let Some(id) = s.maybe_id { Lookup::::remove(id); } Self::deposit_event(Event::Canceled(when, index)); Ok(()) } else { Err(Error::::NotFound)? } } fn do_reschedule( (when, index): TaskAddress, new_time: DispatchTime, ) -> Result, DispatchError> { let new_time = Self::resolve_time(new_time)?; if new_time == when { return Err(Error::::RescheduleNoChange.into()) } Agenda::::try_mutate(when, |agenda| -> DispatchResult { let task = agenda.get_mut(index as usize).ok_or(Error::::NotFound)?; let task = task.take().ok_or(Error::::NotFound)?; Agenda::::append(new_time, Some(task)); Ok(()) })?; let new_index = Agenda::::decode_len(new_time).unwrap_or(1) as u32 - 1; Self::deposit_event(Event::Canceled(when, index)); Self::deposit_event(Event::Scheduled(new_time, new_index)); Ok((new_time, new_index)) } fn do_schedule_named( id: Vec, when: DispatchTime, maybe_periodic: Option>, priority: schedule::Priority, origin: T::PalletsOrigin, call: ::Call, ) -> Result, DispatchError> { // ensure id it is unique if Lookup::::contains_key(&id) { return Err(Error::::FailedToSchedule)? } let when = Self::resolve_time(when)?; // sanitize maybe_periodic let maybe_periodic = maybe_periodic .filter(|p| p.1 > 1 && !p.0.is_zero()) // Remove one from the number of repetitions since we will schedule one now. .map(|(p, c)| (p, c - 1)); let s = Scheduled { maybe_id: Some(id.clone()), priority, call, maybe_periodic, origin, _phantom: Default::default(), }; Agenda::::append(when, Some(s)); let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; if index > T::MaxScheduledPerBlock::get() { log::warn!( target: "runtime::scheduler", "Warning: There are more items queued in the Scheduler than \ expected from the runtime configuration. An update might be needed.", ); } let address = (when, index); Lookup::::insert(&id, &address); Self::deposit_event(Event::Scheduled(when, index)); Ok(address) } fn do_cancel_named(origin: Option, id: Vec) -> DispatchResult { Lookup::::try_mutate_exists(id, |lookup| -> DispatchResult { if let Some((when, index)) = lookup.take() { let i = index as usize; Agenda::::try_mutate(when, |agenda| -> DispatchResult { if let Some(s) = agenda.get_mut(i) { if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) { if *o != s.origin { return Err(BadOrigin.into()) } } *s = None; } Ok(()) })?; Self::deposit_event(Event::Canceled(when, index)); Ok(()) } else { Err(Error::::NotFound)? } }) } fn do_reschedule_named( id: Vec, new_time: DispatchTime, ) -> Result, DispatchError> { let new_time = Self::resolve_time(new_time)?; Lookup::::try_mutate_exists( id, |lookup| -> Result, DispatchError> { let (when, index) = lookup.ok_or(Error::::NotFound)?; if new_time == when { return Err(Error::::RescheduleNoChange.into()) } Agenda::::try_mutate(when, |agenda| -> DispatchResult { let task = agenda.get_mut(index as usize).ok_or(Error::::NotFound)?; let task = task.take().ok_or(Error::::NotFound)?; Agenda::::append(new_time, Some(task)); Ok(()) })?; let new_index = Agenda::::decode_len(new_time).unwrap_or(1) as u32 - 1; Self::deposit_event(Event::Canceled(when, index)); Self::deposit_event(Event::Scheduled(new_time, new_index)); *lookup = Some((new_time, new_index)); Ok((new_time, new_index)) }, ) } } impl schedule::Anon::Call, T::PalletsOrigin> for Pallet { type Address = TaskAddress; fn schedule( when: DispatchTime, maybe_periodic: Option>, priority: schedule::Priority, origin: T::PalletsOrigin, call: ::Call, ) -> Result { Self::do_schedule(when, maybe_periodic, priority, origin, call) } fn cancel((when, index): Self::Address) -> Result<(), ()> { Self::do_cancel(None, (when, index)).map_err(|_| ()) } fn reschedule( address: Self::Address, when: DispatchTime, ) -> Result { Self::do_reschedule(address, when) } fn next_dispatch_time((when, index): Self::Address) -> Result { Agenda::::get(when).get(index as usize).ok_or(()).map(|_| when) } } impl schedule::Named::Call, T::PalletsOrigin> for Pallet { type Address = TaskAddress; fn schedule_named( id: Vec, when: DispatchTime, maybe_periodic: Option>, priority: schedule::Priority, origin: T::PalletsOrigin, call: ::Call, ) -> Result { Self::do_schedule_named(id, when, maybe_periodic, priority, origin, call).map_err(|_| ()) } fn cancel_named(id: Vec) -> Result<(), ()> { Self::do_cancel_named(None, id).map_err(|_| ()) } fn reschedule_named( id: Vec, when: DispatchTime, ) -> Result { Self::do_reschedule_named(id, when) } fn next_dispatch_time(id: Vec) -> Result { Lookup::::get(id) .and_then(|(when, index)| Agenda::::get(when).get(index as usize).map(|_| when)) .ok_or(()) } } #[cfg(test)] mod tests { use super::*; use crate as scheduler; use frame_support::{ assert_err, assert_noop, assert_ok, ord_parameter_types, parameter_types, traits::{Contains, OnFinalize, OnInitialize}, weights::constants::RocksDbWeight, Hashable, }; use frame_system::{EnsureOneOf, EnsureRoot, EnsureSignedBy}; use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, Perbill, }; use substrate_test_utils::assert_eq_uvec; // Logger module to track execution. #[frame_support::pallet] pub mod logger { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use std::cell::RefCell; thread_local! { static LOG: RefCell> = RefCell::new(Vec::new()); } pub fn log() -> Vec<(OriginCaller, u32)> { LOG.with(|log| log.borrow().clone()) } #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(PhantomData); #[pallet::hooks] impl Hooks> for Pallet {} #[pallet::config] pub trait Config: frame_system::Config { type Event: From> + IsType<::Event>; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { Logged(u32, Weight), } #[pallet::call] impl Pallet where ::Origin: OriginTrait, { #[pallet::weight(*weight)] pub fn log(origin: OriginFor, i: u32, weight: Weight) -> DispatchResult { Self::deposit_event(Event::Logged(i, weight)); LOG.with(|log| { log.borrow_mut().push((origin.caller().clone(), i)); }); Ok(()) } #[pallet::weight(*weight)] pub fn log_without_filter( origin: OriginFor, i: u32, weight: Weight, ) -> DispatchResult { Self::deposit_event(Event::Logged(i, weight)); LOG.with(|log| { log.borrow_mut().push((origin.caller().clone(), i)); }); Ok(()) } } } type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( pub enum Test where Block = Block, NodeBlock = Block, UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Pallet, Call, Config, Storage, Event}, Logger: logger::{Pallet, Call, Event}, Scheduler: scheduler::{Pallet, Call, Storage, Event}, } ); // Scheduler must dispatch with root and no filter, this tests base filter is indeed not used. pub struct BaseFilter; impl Contains for BaseFilter { fn contains(call: &Call) -> bool { !matches!(call, Call::Logger(LoggerCall::log { .. })) } } parameter_types! { pub const BlockHashCount: u64 = 250; pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights::simple_max(2_000_000_000_000); } impl system::Config for Test { type BaseCallFilter = BaseFilter; type BlockWeights = (); type BlockLength = (); type DbWeight = RocksDbWeight; type Origin = Origin; type Call = Call; type Index = u64; type BlockNumber = u64; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; type Event = Event; type BlockHashCount = BlockHashCount; type Version = (); type PalletInfo = PalletInfo; type AccountData = (); type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type SS58Prefix = (); type OnSetCode = (); } impl logger::Config for Test { type Event = Event; } parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; pub const MaxScheduledPerBlock: u32 = 10; } ord_parameter_types! { pub const One: u64 = 1; } impl Config for Test { type Event = Event; type Origin = Origin; type PalletsOrigin = OriginCaller; type Call = Call; type MaximumWeight = MaximumSchedulerWeight; type ScheduleOrigin = EnsureOneOf, EnsureSignedBy>; type MaxScheduledPerBlock = MaxScheduledPerBlock; type WeightInfo = (); } pub type LoggerCall = logger::Call; pub fn new_test_ext() -> sp_io::TestExternalities { let t = system::GenesisConfig::default().build_storage::().unwrap(); t.into() } fn run_to_block(n: u64) { while System::block_number() < n { Scheduler::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); Scheduler::on_initialize(System::block_number()); } } fn root() -> OriginCaller { system::RawOrigin::Root.into() } #[test] fn basic_scheduling_works() { new_test_ext().execute_with(|| { let call = Call::Logger(LoggerCall::log { i: 42, weight: 1000 }); assert!(!::BaseCallFilter::contains(&call)); assert_ok!(Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call)); run_to_block(3); assert!(logger::log().is_empty()); run_to_block(4); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32)]); }); } #[test] fn schedule_after_works() { new_test_ext().execute_with(|| { run_to_block(2); let call = Call::Logger(LoggerCall::log { i: 42, weight: 1000 }); assert!(!::BaseCallFilter::contains(&call)); // This will schedule the call 3 blocks after the next block... so block 3 + 3 = 6 assert_ok!(Scheduler::do_schedule(DispatchTime::After(3), None, 127, root(), call)); run_to_block(5); assert!(logger::log().is_empty()); run_to_block(6); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32)]); }); } #[test] fn schedule_after_zero_works() { new_test_ext().execute_with(|| { run_to_block(2); let call = Call::Logger(LoggerCall::log { i: 42, weight: 1000 }); assert!(!::BaseCallFilter::contains(&call)); assert_ok!(Scheduler::do_schedule(DispatchTime::After(0), None, 127, root(), call)); // Will trigger on the next block. run_to_block(3); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32)]); }); } #[test] fn periodic_scheduling_works() { new_test_ext().execute_with(|| { // at #4, every 3 blocks, 3 times. assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), Some((3, 3)), 127, root(), Call::Logger(logger::Call::log { i: 42, weight: 1000 }) )); run_to_block(3); assert!(logger::log().is_empty()); run_to_block(4); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(6); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(7); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); run_to_block(9); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); run_to_block(10); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); }); } #[test] fn reschedule_works() { new_test_ext().execute_with(|| { let call = Call::Logger(LoggerCall::log { i: 42, weight: 1000 }); assert!(!::BaseCallFilter::contains(&call)); assert_eq!( Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call).unwrap(), (4, 0) ); run_to_block(3); assert!(logger::log().is_empty()); assert_eq!(Scheduler::do_reschedule((4, 0), DispatchTime::At(6)).unwrap(), (6, 0)); assert_noop!( Scheduler::do_reschedule((6, 0), DispatchTime::At(6)), Error::::RescheduleNoChange ); run_to_block(4); assert!(logger::log().is_empty()); run_to_block(6); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32)]); }); } #[test] fn reschedule_named_works() { new_test_ext().execute_with(|| { let call = Call::Logger(LoggerCall::log { i: 42, weight: 1000 }); assert!(!::BaseCallFilter::contains(&call)); assert_eq!( Scheduler::do_schedule_named( 1u32.encode(), DispatchTime::At(4), None, 127, root(), call ) .unwrap(), (4, 0) ); run_to_block(3); assert!(logger::log().is_empty()); assert_eq!( Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(6)).unwrap(), (6, 0) ); assert_noop!( Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(6)), Error::::RescheduleNoChange ); run_to_block(4); assert!(logger::log().is_empty()); run_to_block(6); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32)]); }); } #[test] fn reschedule_named_perodic_works() { new_test_ext().execute_with(|| { let call = Call::Logger(LoggerCall::log { i: 42, weight: 1000 }); assert!(!::BaseCallFilter::contains(&call)); assert_eq!( Scheduler::do_schedule_named( 1u32.encode(), DispatchTime::At(4), Some((3, 3)), 127, root(), call ) .unwrap(), (4, 0) ); run_to_block(3); assert!(logger::log().is_empty()); assert_eq!( Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(5)).unwrap(), (5, 0) ); assert_eq!( Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(6)).unwrap(), (6, 0) ); run_to_block(5); assert!(logger::log().is_empty()); run_to_block(6); assert_eq!(logger::log(), vec![(root(), 42u32)]); assert_eq!( Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(10)).unwrap(), (10, 0) ); run_to_block(9); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(10); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); run_to_block(13); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); }); } #[test] fn cancel_named_scheduling_works_with_normal_cancel() { new_test_ext().execute_with(|| { // at #4. Scheduler::do_schedule_named( 1u32.encode(), DispatchTime::At(4), None, 127, root(), Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), ) .unwrap(); let i = Scheduler::do_schedule( DispatchTime::At(4), None, 127, root(), Call::Logger(LoggerCall::log { i: 42, weight: 1000 }), ) .unwrap(); run_to_block(3); assert!(logger::log().is_empty()); assert_ok!(Scheduler::do_cancel_named(None, 1u32.encode())); assert_ok!(Scheduler::do_cancel(None, i)); run_to_block(100); assert!(logger::log().is_empty()); }); } #[test] fn cancel_named_periodic_scheduling_works() { new_test_ext().execute_with(|| { // at #4, every 3 blocks, 3 times. Scheduler::do_schedule_named( 1u32.encode(), DispatchTime::At(4), Some((3, 3)), 127, root(), Call::Logger(LoggerCall::log { i: 42, weight: 1000 }), ) .unwrap(); // same id results in error. assert!(Scheduler::do_schedule_named( 1u32.encode(), DispatchTime::At(4), None, 127, root(), Call::Logger(LoggerCall::log { i: 69, weight: 1000 }) ) .is_err()); // different id is ok. Scheduler::do_schedule_named( 2u32.encode(), DispatchTime::At(8), None, 127, root(), Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), ) .unwrap(); run_to_block(3); assert!(logger::log().is_empty()); run_to_block(4); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(6); assert_ok!(Scheduler::do_cancel_named(None, 1u32.encode())); run_to_block(100); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); }); } #[test] fn scheduler_respects_weight_limits() { new_test_ext().execute_with(|| { assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 127, root(), Call::Logger(LoggerCall::log { i: 42, weight: MaximumSchedulerWeight::get() / 2 }) )); assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 127, root(), Call::Logger(LoggerCall::log { i: 69, weight: MaximumSchedulerWeight::get() / 2 }) )); // 69 and 42 do not fit together run_to_block(4); assert_eq!(logger::log(), vec![(root(), 42u32)]); run_to_block(5); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); }); } #[test] fn scheduler_respects_hard_deadlines_more() { new_test_ext().execute_with(|| { assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 0, root(), Call::Logger(LoggerCall::log { i: 42, weight: MaximumSchedulerWeight::get() / 2 }) )); assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 0, root(), Call::Logger(LoggerCall::log { i: 69, weight: MaximumSchedulerWeight::get() / 2 }) )); // With base weights, 69 and 42 should not fit together, but do because of hard // deadlines run_to_block(4); assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); }); } #[test] fn scheduler_respects_priority_ordering() { new_test_ext().execute_with(|| { assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 1, root(), Call::Logger(LoggerCall::log { i: 42, weight: MaximumSchedulerWeight::get() / 2 }) )); assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 0, root(), Call::Logger(LoggerCall::log { i: 69, weight: MaximumSchedulerWeight::get() / 2 }) )); run_to_block(4); assert_eq!(logger::log(), vec![(root(), 69u32), (root(), 42u32)]); }); } #[test] fn scheduler_respects_priority_ordering_with_soft_deadlines() { new_test_ext().execute_with(|| { assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 255, root(), Call::Logger(LoggerCall::log { i: 42, weight: MaximumSchedulerWeight::get() / 3 }) )); assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 127, root(), Call::Logger(LoggerCall::log { i: 69, weight: MaximumSchedulerWeight::get() / 2 }) )); assert_ok!(Scheduler::do_schedule( DispatchTime::At(4), None, 126, root(), Call::Logger(LoggerCall::log { i: 2600, weight: MaximumSchedulerWeight::get() / 2 }) )); // 2600 does not fit with 69 or 42, but has higher priority, so will go through run_to_block(4); assert_eq!(logger::log(), vec![(root(), 2600u32)]); // 69 and 42 fit together run_to_block(5); assert_eq!(logger::log(), vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)]); }); } #[test] fn on_initialize_weight_is_correct() { new_test_ext().execute_with(|| { let base_weight: Weight = ::DbWeight::get().reads_writes(1, 2); let base_multiplier = 0; let named_multiplier = ::DbWeight::get().writes(1); let periodic_multiplier = ::DbWeight::get().reads_writes(1, 1); // Named assert_ok!(Scheduler::do_schedule_named( 1u32.encode(), DispatchTime::At(1), None, 255, root(), Call::Logger(LoggerCall::log { i: 3, weight: MaximumSchedulerWeight::get() / 3 }) )); // Anon Periodic assert_ok!(Scheduler::do_schedule( DispatchTime::At(1), Some((1000, 3)), 128, root(), Call::Logger(LoggerCall::log { i: 42, weight: MaximumSchedulerWeight::get() / 3 }) )); // Anon assert_ok!(Scheduler::do_schedule( DispatchTime::At(1), None, 127, root(), Call::Logger(LoggerCall::log { i: 69, weight: MaximumSchedulerWeight::get() / 2 }) )); // Named Periodic assert_ok!(Scheduler::do_schedule_named( 2u32.encode(), DispatchTime::At(1), Some((1000, 3)), 126, root(), Call::Logger(LoggerCall::log { i: 2600, weight: MaximumSchedulerWeight::get() / 2 }) )); // Will include the named periodic only let actual_weight = Scheduler::on_initialize(1); let call_weight = MaximumSchedulerWeight::get() / 2; assert_eq!( actual_weight, call_weight + base_weight + base_multiplier + named_multiplier + periodic_multiplier ); assert_eq!(logger::log(), vec![(root(), 2600u32)]); // Will include anon and anon periodic let actual_weight = Scheduler::on_initialize(2); let call_weight = MaximumSchedulerWeight::get() / 2 + MaximumSchedulerWeight::get() / 3; assert_eq!( actual_weight, call_weight + base_weight + base_multiplier * 2 + periodic_multiplier ); assert_eq!(logger::log(), vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)]); // Will include named only let actual_weight = Scheduler::on_initialize(3); let call_weight = MaximumSchedulerWeight::get() / 3; assert_eq!( actual_weight, call_weight + base_weight + base_multiplier + named_multiplier ); assert_eq!( logger::log(), vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32), (root(), 3u32)] ); // Will contain none let actual_weight = Scheduler::on_initialize(4); assert_eq!(actual_weight, 0); }); } #[test] fn root_calls_works() { new_test_ext().execute_with(|| { let call = Box::new(Call::Logger(LoggerCall::log { i: 69, weight: 1000 })); let call2 = Box::new(Call::Logger(LoggerCall::log { i: 42, weight: 1000 })); assert_ok!(Scheduler::schedule_named( Origin::root(), 1u32.encode(), 4, None, 127, call )); assert_ok!(Scheduler::schedule(Origin::root(), 4, None, 127, call2)); run_to_block(3); // Scheduled calls are in the agenda. assert_eq!(Agenda::::get(4).len(), 2); assert!(logger::log().is_empty()); assert_ok!(Scheduler::cancel_named(Origin::root(), 1u32.encode())); assert_ok!(Scheduler::cancel(Origin::root(), 4, 1)); // Scheduled calls are made NONE, so should not effect state run_to_block(100); assert!(logger::log().is_empty()); }); } #[test] fn fails_to_schedule_task_in_the_past() { new_test_ext().execute_with(|| { run_to_block(3); let call = Box::new(Call::Logger(LoggerCall::log { i: 69, weight: 1000 })); let call2 = Box::new(Call::Logger(LoggerCall::log { i: 42, weight: 1000 })); assert_err!( Scheduler::schedule_named(Origin::root(), 1u32.encode(), 2, None, 127, call), Error::::TargetBlockNumberInPast, ); assert_err!( Scheduler::schedule(Origin::root(), 2, None, 127, call2.clone()), Error::::TargetBlockNumberInPast, ); assert_err!( Scheduler::schedule(Origin::root(), 3, None, 127, call2), Error::::TargetBlockNumberInPast, ); }); } #[test] fn should_use_orign() { new_test_ext().execute_with(|| { let call = Box::new(Call::Logger(LoggerCall::log { i: 69, weight: 1000 })); let call2 = Box::new(Call::Logger(LoggerCall::log { i: 42, weight: 1000 })); assert_ok!(Scheduler::schedule_named( system::RawOrigin::Signed(1).into(), 1u32.encode(), 4, None, 127, call )); assert_ok!(Scheduler::schedule( system::RawOrigin::Signed(1).into(), 4, None, 127, call2 )); run_to_block(3); // Scheduled calls are in the agenda. assert_eq!(Agenda::::get(4).len(), 2); assert!(logger::log().is_empty()); assert_ok!(Scheduler::cancel_named(system::RawOrigin::Signed(1).into(), 1u32.encode())); assert_ok!(Scheduler::cancel(system::RawOrigin::Signed(1).into(), 4, 1)); // Scheduled calls are made NONE, so should not effect state run_to_block(100); assert!(logger::log().is_empty()); }); } #[test] fn should_check_orign() { new_test_ext().execute_with(|| { let call = Box::new(Call::Logger(LoggerCall::log { i: 69, weight: 1000 })); let call2 = Box::new(Call::Logger(LoggerCall::log { i: 42, weight: 1000 })); assert_noop!( Scheduler::schedule_named( system::RawOrigin::Signed(2).into(), 1u32.encode(), 4, None, 127, call ), BadOrigin ); assert_noop!( Scheduler::schedule(system::RawOrigin::Signed(2).into(), 4, None, 127, call2), BadOrigin ); }); } #[test] fn should_check_orign_for_cancel() { new_test_ext().execute_with(|| { let call = Box::new(Call::Logger(LoggerCall::log_without_filter { i: 69, weight: 1000 })); let call2 = Box::new(Call::Logger(LoggerCall::log_without_filter { i: 42, weight: 1000 })); assert_ok!(Scheduler::schedule_named( system::RawOrigin::Signed(1).into(), 1u32.encode(), 4, None, 127, call )); assert_ok!(Scheduler::schedule( system::RawOrigin::Signed(1).into(), 4, None, 127, call2 )); run_to_block(3); // Scheduled calls are in the agenda. assert_eq!(Agenda::::get(4).len(), 2); assert!(logger::log().is_empty()); assert_noop!( Scheduler::cancel_named(system::RawOrigin::Signed(2).into(), 1u32.encode()), BadOrigin ); assert_noop!(Scheduler::cancel(system::RawOrigin::Signed(2).into(), 4, 1), BadOrigin); assert_noop!( Scheduler::cancel_named(system::RawOrigin::Root.into(), 1u32.encode()), BadOrigin ); assert_noop!(Scheduler::cancel(system::RawOrigin::Root.into(), 4, 1), BadOrigin); run_to_block(5); assert_eq!( logger::log(), vec![ (system::RawOrigin::Signed(1).into(), 69u32), (system::RawOrigin::Signed(1).into(), 42u32) ] ); }); } #[test] fn migration_to_v2_works() { new_test_ext().execute_with(|| { for i in 0..3u64 { let k = i.twox_64_concat(); let old = vec![ Some(ScheduledV1 { maybe_id: None, priority: i as u8 + 10, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), maybe_periodic: None, }), None, Some(ScheduledV1 { maybe_id: Some(b"test".to_vec()), priority: 123, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), }), ]; frame_support::migration::put_storage_value(b"Scheduler", b"Agenda", &k, old); } assert_eq!(StorageVersion::::get(), Releases::V1); assert!(Scheduler::migrate_v1_to_t2()); assert_eq_uvec!( Agenda::::iter().collect::>(), vec![ ( 0, vec![ Some(ScheduledV2 { maybe_id: None, priority: 10, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), maybe_periodic: None, origin: root(), _phantom: PhantomData::::default(), }), None, Some(ScheduledV2 { maybe_id: Some(b"test".to_vec()), priority: 123, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), origin: root(), _phantom: PhantomData::::default(), }), ] ), ( 1, vec![ Some(ScheduledV2 { maybe_id: None, priority: 11, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), maybe_periodic: None, origin: root(), _phantom: PhantomData::::default(), }), None, Some(ScheduledV2 { maybe_id: Some(b"test".to_vec()), priority: 123, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), origin: root(), _phantom: PhantomData::::default(), }), ] ), ( 2, vec![ Some(ScheduledV2 { maybe_id: None, priority: 12, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), maybe_periodic: None, origin: root(), _phantom: PhantomData::::default(), }), None, Some(ScheduledV2 { maybe_id: Some(b"test".to_vec()), priority: 123, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), origin: root(), _phantom: PhantomData::::default(), }), ] ) ] ); assert_eq!(StorageVersion::::get(), Releases::V2); }); } #[test] fn test_migrate_origin() { new_test_ext().execute_with(|| { for i in 0..3u64 { let k = i.twox_64_concat(); let old: Vec>> = vec![ Some(Scheduled { maybe_id: None, priority: i as u8 + 10, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), origin: 3u32, maybe_periodic: None, _phantom: Default::default(), }), None, Some(Scheduled { maybe_id: Some(b"test".to_vec()), priority: 123, origin: 2u32, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), _phantom: Default::default(), }), ]; frame_support::migration::put_storage_value(b"Scheduler", b"Agenda", &k, old); } impl Into for u32 { fn into(self) -> OriginCaller { match self { 3u32 => system::RawOrigin::Root.into(), 2u32 => system::RawOrigin::None.into(), _ => unreachable!("test make no use of it"), } } } Scheduler::migrate_origin::(); assert_eq_uvec!( Agenda::::iter().collect::>(), vec![ ( 0, vec![ Some(ScheduledV2::<_, _, OriginCaller, u64> { maybe_id: None, priority: 10, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), maybe_periodic: None, origin: system::RawOrigin::Root.into(), _phantom: PhantomData::::default(), }), None, Some(ScheduledV2 { maybe_id: Some(b"test".to_vec()), priority: 123, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), origin: system::RawOrigin::None.into(), _phantom: PhantomData::::default(), }), ] ), ( 1, vec![ Some(ScheduledV2 { maybe_id: None, priority: 11, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), maybe_periodic: None, origin: system::RawOrigin::Root.into(), _phantom: PhantomData::::default(), }), None, Some(ScheduledV2 { maybe_id: Some(b"test".to_vec()), priority: 123, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), origin: system::RawOrigin::None.into(), _phantom: PhantomData::::default(), }), ] ), ( 2, vec![ Some(ScheduledV2 { maybe_id: None, priority: 12, call: Call::Logger(LoggerCall::log { i: 96, weight: 100 }), maybe_periodic: None, origin: system::RawOrigin::Root.into(), _phantom: PhantomData::::default(), }), None, Some(ScheduledV2 { maybe_id: Some(b"test".to_vec()), priority: 123, call: Call::Logger(LoggerCall::log { i: 69, weight: 1000 }), maybe_periodic: Some((456u64, 10)), origin: system::RawOrigin::None.into(), _phantom: PhantomData::::default(), }), ] ) ] ); }); } }