// This file is part of Bizinikiwi. // Copyright (C) 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. //! > Made with *Bizinikiwi*, for *Pezkuwi*. //! //! [![github]](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/bizinikiwi/pezframe/scheduler) - //! [![pezkuwi]](https://pezkuwichain.io) //! //! [pezkuwi]: https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white //! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github //! //! # Scheduler Pezpallet //! //! A Pezpallet for scheduling runtime calls. //! //! ## Overview //! //! This Pezpallet exposes capabilities for scheduling runtime calls to occur at a specified block //! number or at a specified period. These scheduled runtime calls may be named or anonymous and may //! be canceled. //! //! __NOTE:__ Instead of using the filter contained in the origin to call `fn schedule`, scheduled //! runtime calls will be dispatched with the default filter for the origin: namely //! `pezframe_system::Config::BaseCallFilter` for all origin types (except root which will get no //! filter). //! //! If a call is scheduled using proxy or whatever mechanism which adds filter, then those filter //! will not be used when dispatching the schedule runtime call. //! //! ### Examples //! //! 1. Scheduling a runtime call at a specific block. #![doc = docify::embed!("src/tests.rs", basic_scheduling_works)] //! //! 2. Scheduling a preimage hash of a runtime call at a specific block #![doc = docify::embed!("src/tests.rs", scheduling_with_preimages_works)] //! //! ## Pezpallet API //! //! See the [`pezpallet`] module for more information about the interfaces this pezpallet exposes, //! including its configuration trait, dispatchables, storage items, events and errors. //! //! ## Warning //! //! This Pezpallet executes all scheduled runtime calls in the [`on_initialize`] hook. Do not //! execute any runtime calls which should not be considered mandatory. //! //! Please be aware that any scheduled runtime calls executed in a future block may __fail__ or may //! result in __undefined behavior__ since the runtime could have upgraded between the time of //! scheduling and execution. For example, the runtime upgrade could have: //! //! * Modified the implementation of the runtime call (runtime specification upgrade). //! * Could lead to undefined behavior. //! * Removed or changed the ordering/index of the runtime call. //! * Could fail due to the runtime call index not being part of the `Call`. //! * Could lead to undefined behavior, such as executing another runtime call with the same //! index. //! //! [`on_initialize`]: pezframe_support::traits::Hooks::on_initialize // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod migration; #[cfg(test)] mod mock; #[cfg(test)] mod tests; pub mod weights; extern crate alloc; use alloc::{boxed::Box, vec::Vec}; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use core::{borrow::Borrow, cmp::Ordering, marker::PhantomData}; use pezframe_support::{ dispatch::{DispatchResult, GetDispatchInfo, Parameter, RawOrigin}, ensure, traits::{ schedule::{self, DispatchTime, MaybeHashed}, Bounded, CallerTrait, EnsureOrigin, Get, IsType, OriginTrait, PalletInfoAccess, PrivilegeCmp, QueryPreimage, StorageVersion, StorePreimage, }, weights::{Weight, WeightMeter}, }; use pezframe_system::{self as system}; use pezsp_io::hashing::blake2_256; use pezsp_runtime::{ traits::{BadOrigin, BlockNumberProvider, Dispatchable, One, Saturating, Zero}, BoundedVec, DispatchError, RuntimeDebug, }; use scale_info::TypeInfo; pub use pezpallet::*; 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); pub type CallOrHashOf = MaybeHashed<::RuntimeCall, ::Hash>; pub type BoundedCallOf = Bounded<::RuntimeCall, ::Hashing>; pub type BlockNumberFor = <::BlockNumberProvider as BlockNumberProvider>::BlockNumber; /// The configuration of the retry mechanism for a given task along with its current state. #[derive( Clone, Copy, RuntimeDebug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, )] pub struct RetryConfig { /// Initial amount of retries allowed. pub total_retries: u8, /// Amount of retries left. pub remaining: u8, /// Period of time between retry attempts. pub period: Period, } #[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. #[derive( Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, DecodeWithMemTracking, )] pub struct Scheduled { /// The unique identity for this task, if there is one. pub maybe_id: Option, /// This task's priority. pub priority: schedule::Priority, /// The call to be dispatched. pub call: Call, /// If the call is periodic, then this points to the information concerning that. pub maybe_periodic: Option>, /// The origin with which to dispatch the call. pub origin: PalletsOrigin, #[doc(hidden)] pub _phantom: PhantomData, } impl Scheduled where Call: Clone, PalletsOrigin: Clone, { /// Create a new task to be used for retry attempts of the original one. The cloned task will /// have the same `priority`, `call` and `origin`, but will always be non-periodic and unnamed. pub fn as_retry(&self) -> Self { Self { maybe_id: None, priority: self.priority, call: self.call.clone(), maybe_periodic: None, origin: self.origin.clone(), _phantom: Default::default(), } } } use crate::{Scheduled as ScheduledV3, Scheduled as ScheduledV2}; pub type ScheduledV2Of = ScheduledV2< Vec, ::RuntimeCall, BlockNumberFor, ::PalletsOrigin, ::AccountId, >; pub type ScheduledV3Of = ScheduledV3< Vec, CallOrHashOf, BlockNumberFor, ::PalletsOrigin, ::AccountId, >; pub type ScheduledOf = Scheduled< TaskName, BoundedCallOf, BlockNumberFor, ::PalletsOrigin, ::AccountId, >; pub(crate) trait MarginalWeightInfo: WeightInfo { fn service_task(maybe_lookup_len: Option, named: bool, periodic: bool) -> Weight { let base = Self::service_task_base(); let mut total = match maybe_lookup_len { None => base, Some(l) => Self::service_task_fetched(l as u32), }; if named { total.saturating_accrue(Self::service_task_named().saturating_sub(base)); } if periodic { total.saturating_accrue(Self::service_task_periodic().saturating_sub(base)); } total } } impl MarginalWeightInfo for T {} #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; use pezframe_support::{dispatch::PostDispatchInfo, pezpallet_prelude::*}; use pezframe_system::pezpallet_prelude::{BlockNumberFor as SystemBlockNumberFor, OriginFor}; /// The in-code storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); #[pezpallet::pezpallet] #[pezpallet::storage_version(STORAGE_VERSION)] pub struct Pezpallet(_); /// `system::Config` should always be included in our implied traits. #[pezpallet::config] pub trait Config: pezframe_system::Config { /// The overarching event type. #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The aggregated origin which the dispatch will take. type RuntimeOrigin: OriginTrait + From + IsType<::RuntimeOrigin>; /// The caller origin, overarching type of all pallets origins. type PalletsOrigin: From> + CallerTrait + MaxEncodedLen; /// The aggregated call type. type RuntimeCall: Parameter + Dispatchable< RuntimeOrigin = ::RuntimeOrigin, PostInfo = PostDispatchInfo, > + GetDispatchInfo + From>; /// The maximum weight that may be scheduled per block for any dispatchables. #[pezpallet::constant] type MaximumWeight: Get; /// Required origin to schedule or cancel calls. type ScheduleOrigin: EnsureOrigin<::RuntimeOrigin>; /// Compare the privileges of origins. /// /// This will be used when canceling a task, to ensure that the origin that tries /// to cancel has greater or equal privileges as the origin that created the scheduled task. /// /// For simplicity the [`EqualPrivilegeOnly`](pezframe_support::traits::EqualPrivilegeOnly) /// can be used. This will only check if two given origins are equal. type OriginPrivilegeCmp: PrivilegeCmp; /// The maximum number of scheduled calls in the queue for a single block. /// /// NOTE: /// + Dependent pallets' benchmarks might require a higher limit for the setting. Set a /// higher limit under `runtime-benchmarks` feature. #[pezpallet::constant] type MaxScheduledPerBlock: Get; /// Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; /// The preimage provider with which we look up call hashes to get the call. type Preimages: QueryPreimage + StorePreimage; /// Query the current block number. /// /// Must return monotonically increasing values when called from consecutive blocks. It is /// generally expected that the values also do not differ "too much" between consecutive /// blocks. A future addition to this pezpallet will allow bigger difference between /// consecutive blocks to make it possible to be utilized by teyrchains with *Agile /// Coretime*. *Agile Coretime* teyrchains are currently not supported and must continue to /// use their local block number provider. /// /// Can be configured to return either: /// - the local block number of the runtime via `pezframe_system::Pezpallet` /// - a remote block number, eg from the relay chain through `RelaychainDataProvider` /// - an arbitrary value through a custom implementation of the trait /// /// Suggested values: /// - Solo- and Relay-chains should use `pezframe_system::Pezpallet`. There are no concerns /// with this configuration. /// - Teyrchains should also use `pezframe_system::Pezpallet` for the time being. The /// scheduler pezpallet is not yet ready for the case that big numbers of blocks are /// skipped. In an *Agile Coretime* chain with relay chain number provider configured, it /// could otherwise happen that the scheduler will not be able to catch up to its agendas, /// since too many relay blocks are missing if the teyrchain only produces blocks rarely. /// /// There is currently no migration provided to "hot-swap" block number providers and it is /// therefore highly advised to stay with the default (local) values. If you still want to /// swap block number providers on the fly, then please at least ensure that you do not run /// any pezpallet migration in the same runtime upgrade. type BlockNumberProvider: BlockNumberProvider; } /// Block number at which the agenda began incomplete execution. #[pezpallet::storage] pub type IncompleteSince = StorageValue<_, BlockNumberFor>; /// Items to be executed, indexed by the block number that they should be executed on. #[pezpallet::storage] pub type Agenda = StorageMap< _, Twox64Concat, BlockNumberFor, BoundedVec>, T::MaxScheduledPerBlock>, ValueQuery, >; /// Retry configurations for items to be executed, indexed by task address. #[pezpallet::storage] pub type Retries = StorageMap< _, Blake2_128Concat, TaskAddress>, RetryConfig>, OptionQuery, >; /// Lookup from a name to the block number and index of the task. /// /// For v3 -> v4 the previously unbounded identities are Blake2-256 hashed to form the v4 /// identities. #[pezpallet::storage] pub type Lookup = StorageMap<_, Twox64Concat, TaskName, TaskAddress>>; /// Events type. #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Scheduled some task. Scheduled { when: BlockNumberFor, index: u32 }, /// Canceled some task. Canceled { when: BlockNumberFor, index: u32 }, /// Dispatched some task. Dispatched { task: TaskAddress>, id: Option, result: DispatchResult, }, /// Set a retry configuration for some task. RetrySet { task: TaskAddress>, id: Option, period: BlockNumberFor, retries: u8, }, /// Cancel a retry configuration for some task. RetryCancelled { task: TaskAddress>, id: Option }, /// The call for the provided hash was not found so the task has been aborted. CallUnavailable { task: TaskAddress>, id: Option }, /// The given task was unable to be renewed since the agenda is full at that block. PeriodicFailed { task: TaskAddress>, id: Option }, /// The given task was unable to be retried since the agenda is full at that block or there /// was not enough weight to reschedule it. RetryFailed { task: TaskAddress>, id: Option }, /// The given task can never be executed since it is overweight. PermanentlyOverweight { task: TaskAddress>, id: Option }, /// Agenda is incomplete from `when`. AgendaIncomplete { when: BlockNumberFor }, } #[pezpallet::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, /// Attempt to use a non-named function on a named task. Named, } #[pezpallet::hooks] impl Hooks> for Pezpallet { /// Execute the scheduled calls fn on_initialize(_now: SystemBlockNumberFor) -> Weight { let now = T::BlockNumberProvider::current_block_number(); let mut weight_counter = pezframe_system::Pezpallet::::remaining_block_weight() .limit_to(T::MaximumWeight::get()); Self::service_agendas(&mut weight_counter, now, u32::MAX); weight_counter.consumed() } #[cfg(feature = "std")] fn integrity_test() { /// Calculate the maximum weight that a lookup of a given size can take. fn lookup_weight(s: usize) -> Weight { T::WeightInfo::service_agendas_base() + T::WeightInfo::service_agenda_base(T::MaxScheduledPerBlock::get()) + T::WeightInfo::service_task(Some(s), true, true) } let limit = pezsp_runtime::Perbill::from_percent(90) * T::MaximumWeight::get(); let small_lookup = lookup_weight::(128); assert!(small_lookup.all_lte(limit), "Must be possible to submit a small lookup"); let medium_lookup = lookup_weight::(1024); assert!(medium_lookup.all_lte(limit), "Must be possible to submit a medium lookup"); let large_lookup = lookup_weight::(1024 * 1024); assert!(large_lookup.all_lte(limit), "Must be possible to submit a large lookup"); } } #[pezpallet::call] impl Pezpallet { /// Anonymously schedule a task. #[pezpallet::call_index(0)] #[pezpallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] pub fn schedule( origin: OriginFor, when: BlockNumberFor, maybe_periodic: Option>>, priority: schedule::Priority, call: Box<::RuntimeCall>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); Self::do_schedule( DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), T::Preimages::bound(*call)?, )?; Ok(()) } /// Cancel an anonymously scheduled task. #[pezpallet::call_index(1)] #[pezpallet::weight(::WeightInfo::cancel(T::MaxScheduledPerBlock::get()))] pub fn cancel(origin: OriginFor, when: BlockNumberFor, index: u32) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); Self::do_cancel(Some(origin.caller().clone()), (when, index))?; Ok(()) } /// Schedule a named task. #[pezpallet::call_index(2)] #[pezpallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] pub fn schedule_named( origin: OriginFor, id: TaskName, when: BlockNumberFor, maybe_periodic: Option>>, priority: schedule::Priority, call: Box<::RuntimeCall>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); Self::do_schedule_named( id, DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), T::Preimages::bound(*call)?, )?; Ok(()) } /// Cancel a named scheduled task. #[pezpallet::call_index(3)] #[pezpallet::weight(::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get()))] pub fn cancel_named(origin: OriginFor, id: TaskName) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); Self::do_cancel_named(Some(origin.caller().clone()), id)?; Ok(()) } /// Anonymously schedule a task after a delay. #[pezpallet::call_index(4)] #[pezpallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] pub fn schedule_after( origin: OriginFor, after: BlockNumberFor, maybe_periodic: Option>>, priority: schedule::Priority, call: Box<::RuntimeCall>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); Self::do_schedule( DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), T::Preimages::bound(*call)?, )?; Ok(()) } /// Schedule a named task after a delay. #[pezpallet::call_index(5)] #[pezpallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] pub fn schedule_named_after( origin: OriginFor, id: TaskName, after: BlockNumberFor, maybe_periodic: Option>>, priority: schedule::Priority, call: Box<::RuntimeCall>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); Self::do_schedule_named( id, DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), T::Preimages::bound(*call)?, )?; Ok(()) } /// Set a retry configuration for a task so that, in case its scheduled run fails, it will /// be retried after `period` blocks, for a total amount of `retries` retries or until it /// succeeds. /// /// Tasks which need to be scheduled for a retry are still subject to weight metering and /// agenda space, same as a regular task. If a periodic task fails, it will be scheduled /// normally while the task is retrying. /// /// Tasks scheduled as a result of a retry for a periodic task are unnamed, non-periodic /// clones of the original task. Their retry configuration will be derived from the /// original task's configuration, but will have a lower value for `remaining` than the /// original `total_retries`. #[pezpallet::call_index(6)] #[pezpallet::weight(::WeightInfo::set_retry())] pub fn set_retry( origin: OriginFor, task: TaskAddress>, retries: u8, period: BlockNumberFor, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); let (when, index) = task; let agenda = Agenda::::get(when); let scheduled = agenda .get(index as usize) .and_then(Option::as_ref) .ok_or(Error::::NotFound)?; Self::ensure_privilege(origin.caller(), &scheduled.origin)?; Retries::::insert( (when, index), RetryConfig { total_retries: retries, remaining: retries, period }, ); Self::deposit_event(Event::RetrySet { task, id: None, period, retries }); Ok(()) } /// Set a retry configuration for a named task so that, in case its scheduled run fails, it /// will be retried after `period` blocks, for a total amount of `retries` retries or until /// it succeeds. /// /// Tasks which need to be scheduled for a retry are still subject to weight metering and /// agenda space, same as a regular task. If a periodic task fails, it will be scheduled /// normally while the task is retrying. /// /// Tasks scheduled as a result of a retry for a periodic task are unnamed, non-periodic /// clones of the original task. Their retry configuration will be derived from the /// original task's configuration, but will have a lower value for `remaining` than the /// original `total_retries`. #[pezpallet::call_index(7)] #[pezpallet::weight(::WeightInfo::set_retry_named())] pub fn set_retry_named( origin: OriginFor, id: TaskName, retries: u8, period: BlockNumberFor, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); let (when, agenda_index) = Lookup::::get(&id).ok_or(Error::::NotFound)?; let agenda = Agenda::::get(when); let scheduled = agenda .get(agenda_index as usize) .and_then(Option::as_ref) .ok_or(Error::::NotFound)?; Self::ensure_privilege(origin.caller(), &scheduled.origin)?; Retries::::insert( (when, agenda_index), RetryConfig { total_retries: retries, remaining: retries, period }, ); Self::deposit_event(Event::RetrySet { task: (when, agenda_index), id: Some(id), period, retries, }); Ok(()) } /// Removes the retry configuration of a task. #[pezpallet::call_index(8)] #[pezpallet::weight(::WeightInfo::cancel_retry())] pub fn cancel_retry( origin: OriginFor, task: TaskAddress>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); Self::do_cancel_retry(origin.caller(), task)?; Self::deposit_event(Event::RetryCancelled { task, id: None }); Ok(()) } /// Cancel the retry configuration of a named task. #[pezpallet::call_index(9)] #[pezpallet::weight(::WeightInfo::cancel_retry_named())] pub fn cancel_retry_named(origin: OriginFor, id: TaskName) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::RuntimeOrigin::from(origin); let task = Lookup::::get(&id).ok_or(Error::::NotFound)?; Self::do_cancel_retry(origin.caller(), task)?; Self::deposit_event(Event::RetryCancelled { task, id: Some(id) }); Ok(()) } } } impl Pezpallet { /// Migrate storage format from V1 to V4. /// /// Returns the weight consumed by this migration. pub fn migrate_v1_to_v4() -> Weight { use migration::v1 as old; let mut weight = T::DbWeight::get().reads_writes(1, 1); // Delete all undecodable values. // `StorageMap::translate` is not enough since it just skips them and leaves the keys in. let keys = old::Agenda::::iter_keys().collect::>(); for key in keys { weight.saturating_accrue(T::DbWeight::get().reads(1)); if let Err(_) = old::Agenda::::try_get(&key) { weight.saturating_accrue(T::DbWeight::get().writes(1)); old::Agenda::::remove(&key); log::warn!("Deleted undecodable agenda"); } } Agenda::::translate::< Vec::RuntimeCall, BlockNumberFor>>>, _, >(|_, agenda| { Some(BoundedVec::truncate_from( agenda .into_iter() .map(|schedule| { weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); schedule.and_then(|schedule| { if let Some(id) = schedule.maybe_id.as_ref() { let name = blake2_256(id); if let Some(item) = old::Lookup::::take(id) { Lookup::::insert(name, item); } weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } let call = T::Preimages::bound(schedule.call).ok()?; if call.lookup_needed() { weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1)); } Some(Scheduled { maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])), priority: schedule.priority, call, maybe_periodic: schedule.maybe_periodic, origin: system::RawOrigin::Root.into(), _phantom: Default::default(), }) }) }) .collect::>(), )) }); #[allow(deprecated)] pezframe_support::storage::migration::remove_storage_prefix( Self::name().as_bytes(), b"StorageVersion", &[], ); StorageVersion::new(4).put::(); weight + T::DbWeight::get().writes(2) } /// Migrate storage format from V2 to V4. /// /// Returns the weight consumed by this migration. pub fn migrate_v2_to_v4() -> Weight { use migration::v2 as old; let mut weight = T::DbWeight::get().reads_writes(1, 1); // Delete all undecodable values. // `StorageMap::translate` is not enough since it just skips them and leaves the keys in. let keys = old::Agenda::::iter_keys().collect::>(); for key in keys { weight.saturating_accrue(T::DbWeight::get().reads(1)); if let Err(_) = old::Agenda::::try_get(&key) { weight.saturating_accrue(T::DbWeight::get().writes(1)); old::Agenda::::remove(&key); log::warn!("Deleted undecodable agenda"); } } Agenda::::translate::>>, _>(|_, agenda| { Some(BoundedVec::truncate_from( agenda .into_iter() .map(|schedule| { weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); schedule.and_then(|schedule| { if let Some(id) = schedule.maybe_id.as_ref() { let name = blake2_256(id); if let Some(item) = old::Lookup::::take(id) { Lookup::::insert(name, item); } weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } let call = T::Preimages::bound(schedule.call).ok()?; if call.lookup_needed() { weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1)); } Some(Scheduled { maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])), priority: schedule.priority, call, maybe_periodic: schedule.maybe_periodic, origin: schedule.origin, _phantom: Default::default(), }) }) }) .collect::>(), )) }); #[allow(deprecated)] pezframe_support::storage::migration::remove_storage_prefix( Self::name().as_bytes(), b"StorageVersion", &[], ); StorageVersion::new(4).put::(); weight + T::DbWeight::get().writes(2) } /// Migrate storage format from V3 to V4. /// /// Returns the weight consumed by this migration. #[allow(deprecated)] pub fn migrate_v3_to_v4() -> Weight { use migration::v3 as old; let mut weight = T::DbWeight::get().reads_writes(2, 1); // Delete all undecodable values. // `StorageMap::translate` is not enough since it just skips them and leaves the keys in. let blocks = old::Agenda::::iter_keys().collect::>(); for block in blocks { weight.saturating_accrue(T::DbWeight::get().reads(1)); if let Err(_) = old::Agenda::::try_get(&block) { weight.saturating_accrue(T::DbWeight::get().writes(1)); old::Agenda::::remove(&block); log::warn!("Deleted undecodable agenda of block: {:?}", block); } } Agenda::::translate::>>, _>(|block, agenda| { log::info!("Migrating agenda of block: {:?}", &block); Some(BoundedVec::truncate_from( agenda .into_iter() .map(|schedule| { weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); schedule .and_then(|schedule| { if let Some(id) = schedule.maybe_id.as_ref() { let name = blake2_256(id); if let Some(item) = old::Lookup::::take(id) { Lookup::::insert(name, item); log::info!("Migrated name for id: {:?}", id); } else { log::error!("No name in Lookup for id: {:?}", &id); } weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } else { log::info!("Schedule is unnamed"); } let call = match schedule.call { MaybeHashed::Hash(h) => { let bounded = Bounded::from_legacy_hash(h); // Check that the call can be decoded in the new runtime. if let Err(err) = T::Preimages::peek::< ::RuntimeCall, >(&bounded) { log::error!( "Dropping undecodable call {:?}: {:?}", &h, &err ); return None; } weight.saturating_accrue(T::DbWeight::get().reads(1)); log::info!("Migrated call by hash, hash: {:?}", h); bounded }, MaybeHashed::Value(v) => { let call = T::Preimages::bound(v) .map_err(|e| { log::error!("Could not bound Call: {:?}", e) }) .ok()?; if call.lookup_needed() { weight.saturating_accrue( T::DbWeight::get().reads_writes(0, 1), ); } log::info!( "Migrated call by value, hash: {:?}", call.hash() ); call }, }; Some(Scheduled { maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])), priority: schedule.priority, call, maybe_periodic: schedule.maybe_periodic, origin: schedule.origin, _phantom: Default::default(), }) }) .or_else(|| { log::info!("Schedule in agenda for block {:?} is empty - nothing to do here.", &block); None }) }) .collect::>(), )) }); #[allow(deprecated)] pezframe_support::storage::migration::remove_storage_prefix( Self::name().as_bytes(), b"StorageVersion", &[], ); StorageVersion::new(4).put::(); weight + T::DbWeight::get().writes(2) } } impl Pezpallet { /// Helper to migrate scheduler when the pezpallet origin type has changed. pub fn migrate_origin + codec::Decode>() { Agenda::::translate::< Vec< Option< Scheduled< TaskName, BoundedCallOf, BlockNumberFor, OldOrigin, T::AccountId, >, >, >, _, >(|_, agenda| { Some(BoundedVec::truncate_from( 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, DispatchError> { let now = T::BlockNumberProvider::current_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 place_task( when: BlockNumberFor, what: ScheduledOf, ) -> Result>, (DispatchError, ScheduledOf)> { let maybe_name = what.maybe_id; let index = Self::push_to_agenda(when, what)?; let address = (when, index); if let Some(name) = maybe_name { Lookup::::insert(name, address) } Self::deposit_event(Event::Scheduled { when: address.0, index: address.1 }); Ok(address) } fn push_to_agenda( when: BlockNumberFor, what: ScheduledOf, ) -> Result)> { let mut agenda = Agenda::::get(when); let index = if (agenda.len() as u32) < T::MaxScheduledPerBlock::get() { // will always succeed due to the above check. let _ = agenda.try_push(Some(what)); agenda.len() as u32 - 1 } else { if let Some(hole_index) = agenda.iter().position(|i| i.is_none()) { agenda[hole_index] = Some(what); hole_index as u32 } else { return Err((DispatchError::Exhausted, what)); } }; Agenda::::insert(when, agenda); Ok(index) } /// Remove trailing `None` items of an agenda at `when`. If all items are `None` remove the /// agenda record entirely. fn cleanup_agenda(when: BlockNumberFor) { let mut agenda = Agenda::::get(when); match agenda.iter().rposition(|i| i.is_some()) { Some(i) if agenda.len() > i + 1 => { agenda.truncate(i + 1); Agenda::::insert(when, agenda); }, Some(_) => {}, None => { Agenda::::remove(when); }, } } fn do_schedule( when: DispatchTime>, maybe_periodic: Option>>, priority: schedule::Priority, origin: T::PalletsOrigin, call: BoundedCallOf, ) -> Result>, DispatchError> { let when = Self::resolve_time(when)?; let lookup_hash = call.lookup_hash(); // 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 task = Scheduled { maybe_id: None, priority, call, maybe_periodic, origin, _phantom: PhantomData, }; let res = Self::place_task(when, task).map_err(|x| x.0)?; if let Some(hash) = lookup_hash { // Request the call to be made available. T::Preimages::request(&hash); } Ok(res) } 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()) { Self::ensure_privilege(o, &s.origin)?; }; Ok(s.take()) }, ) })?; if let Some(s) = scheduled { T::Preimages::drop(&s.call); if let Some(id) = s.maybe_id { Lookup::::remove(id); } Retries::::remove((when, index)); Self::cleanup_agenda(when); Self::deposit_event(Event::Canceled { when, index }); Ok(()) } else { return Err(Error::::NotFound.into()); } } 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()); } let task = Agenda::::try_mutate(when, |agenda| { let task = agenda.get_mut(index as usize).ok_or(Error::::NotFound)?; ensure!(!matches!(task, Some(Scheduled { maybe_id: Some(_), .. })), Error::::Named); task.take().ok_or(Error::::NotFound) })?; Self::cleanup_agenda(when); Self::deposit_event(Event::Canceled { when, index }); Self::place_task(new_time, task).map_err(|x| x.0) } fn do_schedule_named( id: TaskName, when: DispatchTime>, maybe_periodic: Option>>, priority: schedule::Priority, origin: T::PalletsOrigin, call: BoundedCallOf, ) -> Result>, DispatchError> { // ensure id it is unique if Lookup::::contains_key(&id) { return Err(Error::::FailedToSchedule.into()); } let when = Self::resolve_time(when)?; let lookup_hash = call.lookup_hash(); // 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 task = Scheduled { maybe_id: Some(id), priority, call, maybe_periodic, origin, _phantom: Default::default(), }; let res = Self::place_task(when, task).map_err(|x| x.0)?; if let Some(hash) = lookup_hash { // Request the call to be made available. T::Preimages::request(&hash); } Ok(res) } fn do_cancel_named(origin: Option, id: TaskName) -> 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()) { Self::ensure_privilege(o, &s.origin)?; Retries::::remove((when, index)); T::Preimages::drop(&s.call); } *s = None; } Ok(()) })?; Self::cleanup_agenda(when); Self::deposit_event(Event::Canceled { when, index }); Ok(()) } else { return Err(Error::::NotFound.into()); } }) } fn do_reschedule_named( id: TaskName, new_time: DispatchTime>, ) -> Result>, DispatchError> { let new_time = Self::resolve_time(new_time)?; let lookup = Lookup::::get(id); let (when, index) = lookup.ok_or(Error::::NotFound)?; if new_time == when { return Err(Error::::RescheduleNoChange.into()); } let task = Agenda::::try_mutate(when, |agenda| { let task = agenda.get_mut(index as usize).ok_or(Error::::NotFound)?; task.take().ok_or(Error::::NotFound) })?; Self::cleanup_agenda(when); Self::deposit_event(Event::Canceled { when, index }); Self::place_task(new_time, task).map_err(|x| x.0) } fn do_cancel_retry( origin: &T::PalletsOrigin, (when, index): TaskAddress>, ) -> Result<(), DispatchError> { let agenda = Agenda::::get(when); let scheduled = agenda .get(index as usize) .and_then(Option::as_ref) .ok_or(Error::::NotFound)?; Self::ensure_privilege(origin, &scheduled.origin)?; Retries::::remove((when, index)); Ok(()) } } enum ServiceTaskError { /// Could not be executed due to missing preimage. Unavailable, /// Could not be executed due to weight limitations. Overweight, } use ServiceTaskError::*; impl Pezpallet { /// Service up to `max` agendas queue starting from earliest incompletely executed agenda. fn service_agendas(weight: &mut WeightMeter, now: BlockNumberFor, max: u32) { if weight.try_consume(T::WeightInfo::service_agendas_base()).is_err() { return; } let mut incomplete_since = now + One::one(); let mut when = IncompleteSince::::take().unwrap_or(now); let mut is_first = true; // first task from the first agenda. let max_items = T::MaxScheduledPerBlock::get(); let mut count_down = max; let service_agenda_base_weight = T::WeightInfo::service_agenda_base(max_items); while count_down > 0 && when <= now && weight.can_consume(service_agenda_base_weight) { if !Self::service_agenda(weight, is_first, now, when, u32::MAX) { incomplete_since = incomplete_since.min(when); } is_first = false; when.saturating_inc(); count_down.saturating_dec(); } incomplete_since = incomplete_since.min(when); if incomplete_since <= now { Self::deposit_event(Event::AgendaIncomplete { when: incomplete_since }); IncompleteSince::::put(incomplete_since); } else { // The next scheduler iteration should typically start from `now + 1` (`next_iter_now`). // However, if the [`Config::BlockNumberProvider`] is not a local block number provider, // then `next_iter_now` could be `now + n` where `n > 1`. In this case, we want to start // from `now + 1` to ensure we don't miss any agendas. IncompleteSince::::put(now + One::one()); } } /// Returns `true` if the agenda was fully completed, `false` if it should be revisited at a /// later block. fn service_agenda( weight: &mut WeightMeter, mut is_first: bool, now: BlockNumberFor, when: BlockNumberFor, max: u32, ) -> bool { let mut agenda = Agenda::::get(when); let mut ordered = agenda .iter() .enumerate() .filter_map(|(index, maybe_item)| { maybe_item.as_ref().map(|item| (index as u32, item.priority)) }) .collect::>(); ordered.sort_by_key(|k| k.1); let within_limit = weight .try_consume(T::WeightInfo::service_agenda_base(ordered.len() as u32)) .is_ok(); debug_assert!(within_limit, "weight limit should have been checked in advance"); // Items which we know can be executed and have postponed for execution in a later block. let mut postponed = (ordered.len() as u32).saturating_sub(max); // Items which we don't know can ever be executed. let mut dropped = 0; for (agenda_index, _) in ordered.into_iter().take(max as usize) { let Some(task) = agenda[agenda_index as usize].take() else { continue }; let base_weight = T::WeightInfo::service_task( task.call.lookup_len().map(|x| x as usize), task.maybe_id.is_some(), task.maybe_periodic.is_some(), ); if !weight.can_consume(base_weight) { postponed += 1; agenda[agenda_index as usize] = Some(task); break; } let result = Self::service_task(weight, now, when, agenda_index, is_first, task); agenda[agenda_index as usize] = match result { Err((Unavailable, slot)) => { dropped += 1; slot }, Err((Overweight, slot)) => { postponed += 1; slot }, Ok(()) => { is_first = false; None }, }; } if postponed > 0 || dropped > 0 { Agenda::::insert(when, agenda); } else { Agenda::::remove(when); } postponed == 0 } /// Service (i.e. execute) the given task, being careful not to overflow the `weight` counter. /// /// This involves: /// - removing and potentially replacing the `Lookup` entry for the task. /// - realizing the task's call which can include a preimage lookup. /// - Rescheduling the task for execution in a later agenda if periodic. fn service_task( weight: &mut WeightMeter, now: BlockNumberFor, when: BlockNumberFor, agenda_index: u32, is_first: bool, mut task: ScheduledOf, ) -> Result<(), (ServiceTaskError, Option>)> { if let Some(ref id) = task.maybe_id { Lookup::::remove(id); } let (call, lookup_len) = match T::Preimages::peek(&task.call) { Ok(c) => c, Err(_) => { Self::deposit_event(Event::CallUnavailable { task: (when, agenda_index), id: task.maybe_id, }); // It was not available when we needed it, so we don't need to have requested it // anymore. T::Preimages::drop(&task.call); // We don't know why `peek` failed, thus we most account here for the "full weight". let _ = weight.try_consume(T::WeightInfo::service_task( task.call.lookup_len().map(|x| x as usize), task.maybe_id.is_some(), task.maybe_periodic.is_some(), )); return Err((Unavailable, Some(task))); }, }; let _ = weight.try_consume(T::WeightInfo::service_task( lookup_len.map(|x| x as usize), task.maybe_id.is_some(), task.maybe_periodic.is_some(), )); match Self::execute_dispatch(weight, task.origin.clone(), call) { Err(()) if is_first => { T::Preimages::drop(&task.call); Self::deposit_event(Event::PermanentlyOverweight { task: (when, agenda_index), id: task.maybe_id, }); Err((Unavailable, Some(task))) }, Err(()) => Err((Overweight, Some(task))), Ok(result) => { let failed = result.is_err(); let maybe_retry_config = Retries::::take((when, agenda_index)); Self::deposit_event(Event::Dispatched { task: (when, agenda_index), id: task.maybe_id, result, }); match maybe_retry_config { Some(retry_config) if failed => { Self::schedule_retry(weight, now, when, agenda_index, &task, retry_config); }, _ => {}, } if let &Some((period, count)) = &task.maybe_periodic { if count > 1 { task.maybe_periodic = Some((period, count - 1)); } else { task.maybe_periodic = None; } let wake = now.saturating_add(period); match Self::place_task(wake, task) { Ok(new_address) => { if let Some(retry_config) = maybe_retry_config { Retries::::insert(new_address, retry_config); } }, Err((_, task)) => { // TODO: Leave task in storage somewhere for it to be rescheduled // manually. T::Preimages::drop(&task.call); Self::deposit_event(Event::PeriodicFailed { task: (when, agenda_index), id: task.maybe_id, }); }, } } else { T::Preimages::drop(&task.call); } Ok(()) }, } } /// Make a dispatch to the given `call` from the given `origin`, ensuring that the `weight` /// counter does not exceed its limit and that it is counted accurately (e.g. accounted using /// post info if available). /// /// NOTE: Only the weight for this function will be counted (origin lookup, dispatch and the /// call itself). /// /// Returns an error if the call is overweight. fn execute_dispatch( weight: &mut WeightMeter, origin: T::PalletsOrigin, call: ::RuntimeCall, ) -> Result { let base_weight = match origin.as_system_ref() { Some(&RawOrigin::Signed(_)) => T::WeightInfo::execute_dispatch_signed(), _ => T::WeightInfo::execute_dispatch_unsigned(), }; let call_weight = call.get_dispatch_info().call_weight; // We only allow a scheduled call if it cannot push the weight past the limit. let max_weight = base_weight.saturating_add(call_weight); if !weight.can_consume(max_weight) { return Err(()); } let dispatch_origin = origin.into(); let (maybe_actual_call_weight, result) = match call.dispatch(dispatch_origin) { Ok(post_info) => (post_info.actual_weight, Ok(())), Err(error_and_info) => { (error_and_info.post_info.actual_weight, Err(error_and_info.error)) }, }; let call_weight = maybe_actual_call_weight.unwrap_or(call_weight); let _ = weight.try_consume(base_weight); let _ = weight.try_consume(call_weight); Ok(result) } /// Check if a task has a retry configuration in place and, if so, try to reschedule it. /// /// Possible causes for failure to schedule a retry for a task: /// - there wasn't enough weight to run the task reschedule logic /// - there was no retry configuration in place /// - there were no more retry attempts left /// - the agenda was full. fn schedule_retry( weight: &mut WeightMeter, now: BlockNumberFor, when: BlockNumberFor, agenda_index: u32, task: &ScheduledOf, retry_config: RetryConfig>, ) { if weight .try_consume(T::WeightInfo::schedule_retry(T::MaxScheduledPerBlock::get())) .is_err() { Self::deposit_event(Event::RetryFailed { task: (when, agenda_index), id: task.maybe_id, }); return; } let RetryConfig { total_retries, mut remaining, period } = retry_config; remaining = match remaining.checked_sub(1) { Some(n) => n, None => return, }; let wake = now.saturating_add(period); match Self::place_task(wake, task.as_retry()) { Ok(address) => { // Reinsert the retry config to the new address of the task after it was // placed. Retries::::insert(address, RetryConfig { total_retries, remaining, period }); }, Err((_, task)) => { // TODO: Leave task in storage somewhere for it to be // rescheduled manually. T::Preimages::drop(&task.call); Self::deposit_event(Event::RetryFailed { task: (when, agenda_index), id: task.maybe_id, }); }, } } /// Ensure that `left` has at least the same level of privilege or higher than `right`. /// /// Returns an error if `left` has a lower level of privilege or the two cannot be compared. fn ensure_privilege( left: &::PalletsOrigin, right: &::PalletsOrigin, ) -> Result<(), DispatchError> { if matches!(T::OriginPrivilegeCmp::cmp_privilege(left, right), Some(Ordering::Less) | None) { return Err(BadOrigin.into()); } Ok(()) } } #[allow(deprecated)] impl schedule::v2::Anon, ::RuntimeCall, T::PalletsOrigin> for Pezpallet { type Address = TaskAddress>; type Hash = T::Hash; fn schedule( when: DispatchTime>, maybe_periodic: Option>>, priority: schedule::Priority, origin: T::PalletsOrigin, call: CallOrHashOf, ) -> Result { let call = call.as_value().ok_or(DispatchError::CannotLookup)?; let call = T::Preimages::bound(call)?.transmute(); 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) } } // TODO: migrate `schedule::v2::Anon` to `v3` #[allow(deprecated)] impl schedule::v2::Named, ::RuntimeCall, T::PalletsOrigin> for Pezpallet { type Address = TaskAddress>; type Hash = T::Hash; fn schedule_named( id: Vec, when: DispatchTime>, maybe_periodic: Option>>, priority: schedule::Priority, origin: T::PalletsOrigin, call: CallOrHashOf, ) -> Result { let call = call.as_value().ok_or(())?; let call = T::Preimages::bound(call).map_err(|_| ())?.transmute(); let name = blake2_256(&id[..]); Self::do_schedule_named(name, when, maybe_periodic, priority, origin, call).map_err(|_| ()) } fn cancel_named(id: Vec) -> Result<(), ()> { let name = blake2_256(&id[..]); Self::do_cancel_named(None, name).map_err(|_| ()) } fn reschedule_named( id: Vec, when: DispatchTime>, ) -> Result { let name = blake2_256(&id[..]); Self::do_reschedule_named(name, when) } fn next_dispatch_time(id: Vec) -> Result, ()> { let name = blake2_256(&id[..]); Lookup::::get(name) .and_then(|(when, index)| Agenda::::get(when).get(index as usize).map(|_| when)) .ok_or(()) } } impl schedule::v3::Anon, ::RuntimeCall, T::PalletsOrigin> for Pezpallet { type Address = TaskAddress>; type Hasher = T::Hashing; fn schedule( when: DispatchTime>, maybe_periodic: Option>>, priority: schedule::Priority, origin: T::PalletsOrigin, call: BoundedCallOf, ) -> Result { Self::do_schedule(when, maybe_periodic, priority, origin, call) } fn cancel((when, index): Self::Address) -> Result<(), DispatchError> { Self::do_cancel(None, (when, index)).map_err(map_err_to_v3_err::) } fn reschedule( address: Self::Address, when: DispatchTime>, ) -> Result { Self::do_reschedule(address, when).map_err(map_err_to_v3_err::) } fn next_dispatch_time( (when, index): Self::Address, ) -> Result, DispatchError> { Agenda::::get(when) .get(index as usize) .ok_or(DispatchError::Unavailable) .map(|_| when) } } use schedule::v3::TaskName; impl schedule::v3::Named, ::RuntimeCall, T::PalletsOrigin> for Pezpallet { type Address = TaskAddress>; type Hasher = T::Hashing; fn schedule_named( id: TaskName, when: DispatchTime>, maybe_periodic: Option>>, priority: schedule::Priority, origin: T::PalletsOrigin, call: BoundedCallOf, ) -> Result { Self::do_schedule_named(id, when, maybe_periodic, priority, origin, call) } fn cancel_named(id: TaskName) -> Result<(), DispatchError> { Self::do_cancel_named(None, id).map_err(map_err_to_v3_err::) } fn reschedule_named( id: TaskName, when: DispatchTime>, ) -> Result { Self::do_reschedule_named(id, when).map_err(map_err_to_v3_err::) } fn next_dispatch_time(id: TaskName) -> Result, DispatchError> { Lookup::::get(id) .and_then(|(when, index)| Agenda::::get(when).get(index as usize).map(|_| when)) .ok_or(DispatchError::Unavailable) } } /// Maps a pezpallet error to an `schedule::v3` error. fn map_err_to_v3_err(err: DispatchError) -> DispatchError { if err == DispatchError::from(Error::::NotFound) { DispatchError::Unavailable } else { err } }