diff --git a/Cargo.lock b/Cargo.lock index c2d53ccd9c4c5..c7c97f84b1279 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3833,6 +3833,7 @@ dependencies = [ "sp-version", "static_assertions", "substrate-wasm-builder", + "tasks-example", ] [[package]] @@ -11766,6 +11767,21 @@ version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" +[[package]] +name = "tasks-example" +version = "4.0.0-dev" +dependencies = [ + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "tempfile" version = "3.6.0" diff --git a/Cargo.toml b/Cargo.toml index 9ee8142e23e76..e3e48072e413f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ members = [ "frame/examples/dev-mode", "frame/examples/split", "frame/examples/default-config", + "frame/examples/tasks-example", "frame/executive", "frame/nis", "frame/grandpa", diff --git a/bin/node-template/pallets/template/src/mock.rs b/bin/node-template/pallets/template/src/mock.rs index 244ae1b37859b..39c58d03efacb 100644 --- a/bin/node-template/pallets/template/src/mock.rs +++ b/bin/node-template/pallets/template/src/mock.rs @@ -24,6 +24,7 @@ impl frame_system::Config for Test { type DbWeight = (); type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; type Nonce = u64; type Hash = H256; type Hashing = BlakeTwo256; diff --git a/bin/node-template/runtime/src/lib.rs b/bin/node-template/runtime/src/lib.rs index c3375d2ee601a..469f5218f5d27 100644 --- a/bin/node-template/runtime/src/lib.rs +++ b/bin/node-template/runtime/src/lib.rs @@ -165,6 +165,8 @@ impl frame_system::Config for Runtime { type AccountId = AccountId; /// The aggregated dispatch type that is available for extrinsics. type RuntimeCall = RuntimeCall; + /// The aggregated task type that is available for extrinsics. + type RuntimeTask = RuntimeTask; /// The lookup mechanism to get account ID from whatever is passed in dispatchers. type Lookup = AccountIdLookup; /// The type for storing how many extrinsics an account has signed. diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index de0ec1a5489a8..255a495489158 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -127,6 +127,7 @@ pallet-transaction-storage = { version = "4.0.0-dev", default-features = false, pallet-uniques = { version = "4.0.0-dev", default-features = false, path = "../../../frame/uniques" } pallet-vesting = { version = "4.0.0-dev", default-features = false, path = "../../../frame/vesting" } pallet-whitelist = { version = "4.0.0-dev", default-features = false, path = "../../../frame/whitelist" } +tasks-example = { version = "4.0.0-dev", path = '../../../frame/examples/tasks-example', default-features = false } [build-dependencies] substrate-wasm-builder = { version = "5.0.0-dev", path = "../../../utils/wasm-builder", optional = true } diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 29aaaf5fdad5a..3ed10df2ba569 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -83,6 +83,7 @@ use sp_std::prelude::*; use sp_version::NativeVersion; use sp_version::RuntimeVersion; use static_assertions::const_assert; +use tasks_example; #[cfg(any(feature = "std", test))] pub use frame_system::Call as SystemCall; @@ -224,6 +225,7 @@ impl frame_system::Config for Runtime { type DbWeight = RocksDbWeight; type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; type Nonce = Nonce; type Hash = Hash; type Hashing = BlakeTwo256; @@ -245,6 +247,11 @@ impl frame_system::Config for Runtime { impl pallet_insecure_randomness_collective_flip::Config for Runtime {} +impl tasks_example::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeTask = RuntimeTask; +} + impl pallet_utility::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -1946,6 +1953,7 @@ construct_runtime!( MessageQueue: pallet_message_queue, Pov: frame_benchmarking_pallet_pov, Statement: pallet_statement, + TasksExample: tasks_example::{Pallet, Storage, Call, Event, Task}, } ); @@ -2031,6 +2039,7 @@ mod benches { [pallet_conviction_voting, ConvictionVoting] [pallet_contracts, Contracts] [pallet_core_fellowship, CoreFellowship] + [tasks_example, TasksExample] [pallet_democracy, Democracy] [pallet_asset_conversion, AssetConversion] [pallet_election_provider_multi_phase, ElectionProviderMultiPhase] diff --git a/frame/examples/tasks-example/Cargo.toml b/frame/examples/tasks-example/Cargo.toml new file mode 100644 index 0000000000000..d90de4fae9602 --- /dev/null +++ b/frame/examples/tasks-example/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "tasks-example" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "MIT-0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME example pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../../system" } +sp-io = { version = "23.0.0", default-features = false, path = "../../../primitives/io" } +sp-runtime = { version = "24.0.0", default-features = false, path = "../../../primitives/runtime" } +sp-std = { version = "8.0.0", default-features = false, path = "../../../primitives/std" } +sp-core = { version = "21.0.0", default-features = false, path = "../../../primitives/core" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "sp-core/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/examples/tasks-example/src/lib.rs b/frame/examples/tasks-example/src/lib.rs new file mode 100644 index 0000000000000..2223ecbd0d30c --- /dev/null +++ b/frame/examples/tasks-example/src/lib.rs @@ -0,0 +1,179 @@ +// This file is part of Substrate. + +// 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. + +#![cfg_attr(not(feature = "std"), no_std)] + +use core::marker::PhantomData; + +use codec::{Decode, Encode}; +use frame_support::dispatch::DispatchResult; +// Re-export pallet items so that they can be accessed from the crate namespace. +pub use pallet::*; +use sp_runtime::DispatchError; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + + use super::*; + use frame_support::{pallet_prelude::*, traits::AggregatedTask}; + use frame_system::pallet_prelude::*; + + // this can be auto-generated from the macros + #[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] + pub enum Task { + Increment, + Decrement, + #[doc(hidden)] + #[codec(skip)] + __Ignore(PhantomData, frame_support::Never), + } + + // this can be auto-generated from the macros and will always be the same + impl core::fmt::Debug for Task { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Task").field("value", self).finish() + } + } + + // we can automatically inject `InvalidTask` into an existing Error enum by finding it via + // visitor pattern, otherwise we can just emit an error enum containing just our + // `InvalidTask` variant. Alternatively we could just expect that `InvalidTask` is included + // in the error enum, by convention, or we could use something like + // `InvalidTransaction::Custom(1u8)` but this seems bad to me. + #[pallet::error] + pub enum Error { + InvalidTask, + ValueOverflow, + ValueUnderflow, + } + + // this will be auto-generated from `#[pallet::tasks]` + impl frame_support::traits::Task for Task + where + T: TypeInfo, + { + type Enumeration = sp_std::vec::IntoIter>; + + const TASK_INDEX: u64 = 0; + + fn enumerate() -> Self::Enumeration { + sp_std::vec![Task::Increment, Task::Decrement].into_iter() + } + + fn is_valid(&self) -> bool { + let value = Value::::get().unwrap(); + match self { + Task::Increment => value < 255, + Task::Decrement => value > 0, + Task::__Ignore(_, _) => unreachable!(), + } + } + + fn run(&self) -> Result<(), DispatchError> { + match self { + Task::Increment => { + // Get the value and check if it can be incremented + let value = Value::::get().unwrap_or_default(); + if value >= 255 { + Err(Error::::ValueOverflow.into()) + } else { + let new_val = value.checked_add(1).ok_or(Error::::ValueOverflow)?; + Value::::put(new_val); + Pallet::::deposit_event(Event::Incremented { new_val }); + Ok(()) + } + }, + Task::Decrement => { + // Get the value and check if it can be decremented + let value = Value::::get().unwrap_or_default(); + if value == 0 { + Err(Error::::ValueUnderflow.into()) + } else { + let new_val = value.checked_sub(1).ok_or(Error::::ValueUnderflow)?; + Value::::put(new_val); + Pallet::::deposit_event(Event::Decremented { new_val }); + Ok(()) + } + }, + Task::__Ignore(_, _) => unreachable!(), + } + } + + fn weight(&self) -> Weight { + Weight::default() + } + } + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type RuntimeTask: AggregatedTask; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn value)] + pub type Value = StorageValue<_, u8>; + + #[pallet::call] + impl Pallet + where + T: TypeInfo, + { + pub fn increment(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + + // Increment the value and emit an event + let new_val = Value::::get().unwrap().checked_add(1).ok_or("Value overflow")?; + Value::::put(new_val); + Self::deposit_event(Event::Incremented { new_val }); + + Ok(()) + } + + pub fn decrement(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + + // Decrement the value and emit an event + let new_val = Value::::get().unwrap().checked_sub(1).ok_or("Value underflow")?; + Value::::put(new_val); + Self::deposit_event(Event::Decremented { new_val }); + + Ok(()) + } + + // this will be auto-generated by the macros and will always be the same + pub fn do_task(origin: OriginFor, task: Task) -> DispatchResult { + use frame_support::traits::Task; + ensure_root(origin)?; + if task.is_valid() { + task.run() + } else { + Err(Error::::InvalidTask.into()) + } + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Incremented { new_val: u8 }, + Decremented { new_val: u8 }, + } +} diff --git a/frame/support/procedural/src/construct_runtime/expand/mod.rs b/frame/support/procedural/src/construct_runtime/expand/mod.rs index 830338f9265ff..9dab9f664094a 100644 --- a/frame/support/procedural/src/construct_runtime/expand/mod.rs +++ b/frame/support/procedural/src/construct_runtime/expand/mod.rs @@ -25,6 +25,7 @@ mod metadata; mod origin; mod outer_enums; mod slash_reason; +mod task; mod unsigned; pub use call::expand_outer_dispatch; @@ -37,4 +38,5 @@ pub use metadata::expand_runtime_metadata; pub use origin::expand_outer_origin; pub use outer_enums::{expand_outer_enum, OuterEnumType}; pub use slash_reason::expand_outer_slash_reason; +pub use task::expand_outer_task; pub use unsigned::expand_outer_validate_unsigned; diff --git a/frame/support/procedural/src/construct_runtime/expand/task.rs b/frame/support/procedural/src/construct_runtime/expand/task.rs new file mode 100644 index 0000000000000..8d857ee4c07ef --- /dev/null +++ b/frame/support/procedural/src/construct_runtime/expand/task.rs @@ -0,0 +1,106 @@ +// This file is part of Substrate. + +// 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 + +use crate::construct_runtime::{parse::PalletPath, Pallet}; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +/// Expands aggregate `RuntimeTask` enum. +pub fn expand_outer_task(pallet_decls: &[Pallet], scrate: &TokenStream) -> TokenStream { + let mut conversion_fns = Vec::new(); + let mut task_variants = Vec::new(); + for decl in pallet_decls { + if let Some(_) = decl.find_part("Task") { + // TODO: for some reason `find_part` above never finds `Task` even when it is + // clearly in the pallet + let variant_name = &decl.name; + let path = &decl.path; + let index = decl.index; + + conversion_fns.push(expand_conversion_fn(path, variant_name)); + + task_variants.push(expand_variant(index, path, variant_name)); + } + } + use quote::ToTokens; + if !task_variants.is_empty() { + println!( + "{:#?}", + task_variants + .iter() + .map(|item| item.to_token_stream().to_string()) + .collect::>() + ); + } + + quote! { + /// An aggregation of all `Task` enums across all pallets included in the current runtime. + #[derive( + Clone, Eq, PartialEq, + // Ord, PartialOrd, + #scrate::codec::Encode, #scrate::codec::Decode, + // #scrate::codec::MaxEncodedLen, + #scrate::scale_info::TypeInfo, + #scrate::RuntimeDebug, + )] + pub enum RuntimeTask { + #( #task_variants )* + } + + impl #scrate::traits::AggregatedTask for RuntimeTask { + fn is_valid(&self) -> bool { + use #scrate::traits::tasks::prelude::*; + todo!(); + } + + fn run(&self) -> Result<(), #scrate::traits::tasks::prelude::DispatchError> { + todo!(); + } + + fn weight(&self) -> Weight { + todo!(); + } + + fn task_index(&self) -> u64 { + todo!(); + } + } + + #( #conversion_fns )* + } +} + +fn expand_conversion_fn(path: &PalletPath, variant_name: &Ident) -> TokenStream { + // Todo: Replace `Runtime` with the actual runtime ident + // `pallet` will probably not be needed when `Task` is generated by macro + quote! { + impl From<#path::pallet::Task> for RuntimeTask { + fn from(hr: #path::pallet::Task) -> Self { + RuntimeTask::#variant_name(hr) + } + } + } +} + +fn expand_variant(index: u8, path: &PalletPath, variant_name: &Ident) -> TokenStream { + // Todo: Replace `Runtime` with the actual runtime ident + // `pallet` will probably not be needed when `Task` is generated by macro + quote! { + #[codec(index = #index)] + #variant_name(#path::pallet::Task), + } +} diff --git a/frame/support/procedural/src/construct_runtime/mod.rs b/frame/support/procedural/src/construct_runtime/mod.rs index efc2244154479..c4a3c0cbd5a35 100644 --- a/frame/support/procedural/src/construct_runtime/mod.rs +++ b/frame/support/procedural/src/construct_runtime/mod.rs @@ -385,6 +385,7 @@ fn construct_runtime_final_expansion( let pallet_to_index = decl_pallet_runtime_setup(&name, &pallets, &scrate); let dispatch = expand::expand_outer_dispatch(&name, system_pallet, &pallets, &scrate); + let tasks = expand::expand_outer_task(&pallets, &scrate); let metadata = expand::expand_runtime_metadata( &name, &pallets, @@ -472,6 +473,8 @@ fn construct_runtime_final_expansion( #dispatch + #tasks + #metadata #outer_config diff --git a/frame/support/procedural/src/construct_runtime/parse.rs b/frame/support/procedural/src/construct_runtime/parse.rs index 9b08e16469754..88f3f14dc86c5 100644 --- a/frame/support/procedural/src/construct_runtime/parse.rs +++ b/frame/support/procedural/src/construct_runtime/parse.rs @@ -42,6 +42,7 @@ mod keyword { syn::custom_keyword!(ValidateUnsigned); syn::custom_keyword!(FreezeReason); syn::custom_keyword!(HoldReason); + syn::custom_keyword!(Task); syn::custom_keyword!(LockId); syn::custom_keyword!(SlashReason); syn::custom_keyword!(exclude_parts); @@ -404,6 +405,7 @@ pub enum PalletPartKeyword { ValidateUnsigned(keyword::ValidateUnsigned), FreezeReason(keyword::FreezeReason), HoldReason(keyword::HoldReason), + Task(keyword::Task), LockId(keyword::LockId), SlashReason(keyword::SlashReason), } @@ -434,6 +436,8 @@ impl Parse for PalletPartKeyword { Ok(Self::FreezeReason(input.parse()?)) } else if lookahead.peek(keyword::HoldReason) { Ok(Self::HoldReason(input.parse()?)) + } else if lookahead.peek(keyword::Task) { + Ok(Self::Task(input.parse()?)) } else if lookahead.peek(keyword::LockId) { Ok(Self::LockId(input.parse()?)) } else if lookahead.peek(keyword::SlashReason) { @@ -459,6 +463,7 @@ impl PalletPartKeyword { Self::ValidateUnsigned(_) => "ValidateUnsigned", Self::FreezeReason(_) => "FreezeReason", Self::HoldReason(_) => "HoldReason", + Self::Task(_) => "Task", Self::LockId(_) => "LockId", Self::SlashReason(_) => "SlashReason", } @@ -471,7 +476,7 @@ impl PalletPartKeyword { /// Returns the names of all pallet parts that allow to have a generic argument. fn all_generic_arg() -> &'static [&'static str] { - &["Event", "Error", "Origin", "Config"] + &["Event", "Error", "Origin", "Config", "Task"] } } @@ -489,6 +494,7 @@ impl ToTokens for PalletPartKeyword { Self::ValidateUnsigned(inner) => inner.to_tokens(tokens), Self::FreezeReason(inner) => inner.to_tokens(tokens), Self::HoldReason(inner) => inner.to_tokens(tokens), + Self::Task(inner) => inner.to_tokens(tokens), Self::LockId(inner) => inner.to_tokens(tokens), Self::SlashReason(inner) => inner.to_tokens(tokens), } diff --git a/frame/support/procedural/src/lib.rs b/frame/support/procedural/src/lib.rs index f0d4c30a939c1..707295e70af16 100644 --- a/frame/support/procedural/src/lib.rs +++ b/frame/support/procedural/src/lib.rs @@ -633,7 +633,6 @@ pub fn storage_alias(_: TokenStream, input: TokenStream) -> TokenStream { /// ``` /// /// where `TestDefaultConfig` was defined and registered as follows: -/// /// ```ignore /// pub struct TestDefaultConfig; /// @@ -660,7 +659,6 @@ pub fn storage_alias(_: TokenStream, input: TokenStream) -> TokenStream { /// ``` /// /// The above call to `derive_impl` would expand to roughly the following: -/// /// ```ignore /// impl frame_system::Config for Test { /// use frame_system::config_preludes::TestDefaultConfig; diff --git a/frame/support/procedural/src/pallet/expand/tt_default_parts.rs b/frame/support/procedural/src/pallet/expand/tt_default_parts.rs index 356bdbf67e923..704151e6ec450 100644 --- a/frame/support/procedural/src/pallet/expand/tt_default_parts.rs +++ b/frame/support/procedural/src/pallet/expand/tt_default_parts.rs @@ -31,6 +31,19 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream { let call_part = def.call.as_ref().map(|_| quote::quote!(Call,)); + // TODO: maybe allow `task_enum: Option` to sit on `Def` itself? + let task_part = def.item.content.as_ref().and_then(|(_, items)| { + items.iter().find_map(|item| { + if let syn::Item::Enum(item_enum) = item { + if item_enum.ident == "Task" { + println!("found task enum in tt_default_parts!"); + return Some(item_enum) + } + } + None + }) + }); + let storage_part = (!def.storages.is_empty()).then(|| quote::quote!(Storage,)); let event_part = def.event.as_ref().map(|event| { @@ -101,7 +114,7 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream { tokens = [{ expanded::{ Pallet, #call_part #storage_part #event_part #error_part #origin_part #config_part - #inherent_part #validate_unsigned_part #freeze_reason_part + #inherent_part #validate_unsigned_part #freeze_reason_part #task_part #hold_reason_part #lock_id_part #slash_reason_part } }] diff --git a/frame/support/procedural/src/pallet/parse/composite.rs b/frame/support/procedural/src/pallet/parse/composite.rs index 2bbfcd2e998ab..04b853e62dc4c 100644 --- a/frame/support/procedural/src/pallet/parse/composite.rs +++ b/frame/support/procedural/src/pallet/parse/composite.rs @@ -25,11 +25,14 @@ pub mod keyword { syn::custom_keyword!(HoldReason); syn::custom_keyword!(LockId); syn::custom_keyword!(SlashReason); + syn::custom_keyword!(Task); + pub enum CompositeKeyword { FreezeReason(FreezeReason), HoldReason(HoldReason), LockId(LockId), SlashReason(SlashReason), + Task(Task), } impl ToTokens for CompositeKeyword { @@ -40,6 +43,7 @@ pub mod keyword { HoldReason(inner) => inner.to_tokens(tokens), LockId(inner) => inner.to_tokens(tokens), SlashReason(inner) => inner.to_tokens(tokens), + Task(inner) => inner.to_tokens(tokens), } } } @@ -55,6 +59,8 @@ pub mod keyword { Ok(Self::LockId(input.parse()?)) } else if lookahead.peek(SlashReason) { Ok(Self::SlashReason(input.parse()?)) + } else if lookahead.peek(Task) { + Ok(Self::Task(input.parse()?)) } else { Err(lookahead.error()) } @@ -70,6 +76,7 @@ pub mod keyword { match self { FreezeReason(_) => "FreezeReason", HoldReason(_) => "HoldReason", + Task(_) => "Task", LockId(_) => "LockId", SlashReason(_) => "SlashReason", } @@ -79,7 +86,7 @@ pub mod keyword { } pub struct CompositeDef { - /// The index of the HoldReason item in the pallet module. + /// The index of the CompositeDef item in the pallet module. pub index: usize, /// The composite keyword used (contains span). pub composite_keyword: keyword::CompositeKeyword, diff --git a/frame/support/procedural/src/pallet/parse/mod.rs b/frame/support/procedural/src/pallet/parse/mod.rs index 0f5e5f1136610..44bf12dc19f79 100644 --- a/frame/support/procedural/src/pallet/parse/mod.rs +++ b/frame/support/procedural/src/pallet/parse/mod.rs @@ -33,6 +33,7 @@ pub mod inherent; pub mod origin; pub mod pallet_struct; pub mod storage; +pub mod tasks; pub mod type_value; pub mod validate_unsigned; @@ -49,6 +50,8 @@ pub struct Def { pub pallet_struct: pallet_struct::PalletStructDef, pub hooks: Option, pub call: Option, + pub tasks: Option, + pub task_enum: Option, pub storages: Vec, pub error: Option, pub event: Option, @@ -84,6 +87,8 @@ impl Def { let mut pallet_struct = None; let mut hooks = None; let mut call = None; + let mut tasks = None; + let mut task_enum = None; let mut error = None; let mut event = None; let mut origin = None; @@ -97,6 +102,14 @@ impl Def { let mut composites: Vec = vec![]; for (index, item) in items.iter_mut().enumerate() { + // find manually specified `Task` enum, if present + if let syn::Item::Enum(item_enum) = item { + if item_enum.ident == "Task" { + println!("found task enum while parsing Def!"); + task_enum = Some(item_enum.clone()); + } + } + let pallet_attr: Option = helper::take_first_item_pallet_attr(item)?; match pallet_attr { @@ -118,6 +131,8 @@ impl Def { }, Some(PalletAttr::RuntimeCall(cw, span)) if call.is_none() => call = Some(call::CallDef::try_from(span, index, item, dev_mode, cw)?), + Some(PalletAttr::Tasks(span)) if tasks.is_none() => + tasks = Some(tasks::TasksDef::try_from(span, index, item)?), Some(PalletAttr::Error(span)) if error.is_none() => error = Some(error::ErrorDef::try_from(span, index, item)?), Some(PalletAttr::RuntimeEvent(span)) if event.is_none() => @@ -198,6 +213,8 @@ impl Def { .ok_or_else(|| syn::Error::new(item_span, "Missing `#[pallet::pallet]`"))?, hooks, call, + tasks, + task_enum, extra_constants, genesis_config, genesis_build, @@ -408,6 +425,7 @@ impl GenericKind { mod keyword { syn::custom_keyword!(origin); syn::custom_keyword!(call); + syn::custom_keyword!(tasks); syn::custom_keyword!(weight); syn::custom_keyword!(event); syn::custom_keyword!(config); @@ -471,6 +489,7 @@ enum PalletAttr { /// to zero. Now when there is a `weight` attribute on the `#[pallet::call]`, then that is used /// instead of the zero weight. So to say: it works together with `dev_mode`. RuntimeCall(Option, proc_macro2::Span), + Tasks(proc_macro2::Span), Error(proc_macro2::Span), RuntimeEvent(proc_macro2::Span), RuntimeOrigin(proc_macro2::Span), @@ -491,6 +510,7 @@ impl PalletAttr { Self::Pallet(span) => *span, Self::Hooks(span) => *span, Self::RuntimeCall(_, span) => *span, + Self::Tasks(span) => *span, Self::Error(span) => *span, Self::RuntimeEvent(span) => *span, Self::RuntimeOrigin(span) => *span, @@ -535,6 +555,8 @@ impl syn::parse::Parse for PalletAttr { false => Some(InheritedCallWeightAttr::parse(&content)?), }; Ok(PalletAttr::RuntimeCall(attr, span)) + } else if lookahead.peek(keyword::tasks) { + Ok(PalletAttr::Tasks(content.parse::()?.span())) } else if lookahead.peek(keyword::error) { Ok(PalletAttr::Error(content.parse::()?.span())) } else if lookahead.peek(keyword::event) { diff --git a/frame/support/procedural/src/pallet/parse/tasks.rs b/frame/support/procedural/src/pallet/parse/tasks.rs new file mode 100644 index 0000000000000..23715014585aa --- /dev/null +++ b/frame/support/procedural/src/pallet/parse/tasks.rs @@ -0,0 +1,121 @@ +// This file is part of Substrate. + +// 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. + +use derive_syn_parse::Parse; +use proc_macro2::Span; +use syn::{ + token::{Bracket, Paren}, + Expr, Ident, Item, LitInt, Result, Token, +}; + +pub mod keywords { + use syn::custom_keyword; + + custom_keyword!(tasks); + custom_keyword!(task_list); + custom_keyword!(condition); + custom_keyword!(task_index); + custom_keyword!(pallet); +} + +pub struct TasksDef; + +impl TasksDef { + pub fn try_from(_span: Span, _index: usize, _item: &mut Item) -> Result { + // TODO: fill in + Ok(TasksDef {}) + } +} + +#[derive(Parse)] +pub enum TaskAttrType { + #[peek(keywords::task_list, name = "#[pallet::task_list(..)]")] + TaskList { + _tasks: keywords::task_list, + #[paren] + _paren: Paren, + #[inside(_paren)] + expr: Expr, + }, + #[peek(keywords::task_index, name = "#[pallet::task_index(..)")] + TaskIndex { + _task_index: keywords::task_index, + #[paren] + _paren: Paren, + #[inside(_paren)] + index: LitInt, + }, + #[peek(keywords::condition, name = "#[pallet::condition(..)")] + Condition { + _condition: keywords::condition, + #[paren] + _paren: Paren, + #[inside(_paren)] + _pipe1: Token![|], + #[inside(_paren)] + _ident: Ident, + #[inside(_paren)] + _pipe2: Token![|], + #[inside(_paren)] + expr: Expr, + }, + // TODO: Tasks +} + +#[derive(Parse)] +pub struct PalletTaskAttr { + _pound: Token![#], + #[bracket] + _bracket: Bracket, + #[inside(_bracket)] + _pallet: keywords::pallet, + #[inside(_bracket)] + _colons: Token![::], + #[inside(_bracket)] + _attr: TaskAttrType, +} + +#[cfg(test)] +use syn::parse2; + +#[cfg(test)] +use quote::quote; + +#[test] +fn test_parse_pallet_task_list_() { + parse2::(quote!(#[pallet::task_list(Something::iter())])).unwrap(); + assert!(parse2::(quote!(#[pallet::task_list()])).is_err()); + assert!(parse2::(quote!(#[pallet::tasks_list(iter())])).is_err()); + assert!(parse2::(quote!(#[pallet::task_list])).is_err()); +} + +#[test] +fn test_parse_pallet_task_index() { + parse2::(quote!(#[pallet::task_index(3)])).unwrap(); + parse2::(quote!(#[pallet::task_index(0)])).unwrap(); + parse2::(quote!(#[pallet::task_index(17)])).unwrap(); + assert!(parse2::(quote!(#[pallet::task_index])).is_err()); + assert!(parse2::(quote!(#[pallet::task_index("hey")])).is_err()); +} + +#[test] +fn test_parse_pallet_condition() { + parse2::(quote!(#[pallet::condition(|x| x.is_some())])).unwrap(); + parse2::(quote!(#[pallet::condition(|_x| some_expr())])).unwrap(); + assert!(parse2::(quote!(#[pallet::condition(x.is_some())])).is_err()); + assert!(parse2::(quote!(#[pallet::condition(|| something())])).is_err()); +} diff --git a/frame/support/src/dispatch.rs b/frame/support/src/dispatch.rs index e462066a84605..9233c6df66a97 100644 --- a/frame/support/src/dispatch.rs +++ b/frame/support/src/dispatch.rs @@ -684,6 +684,7 @@ mod weight_tests { type BaseCallFilter: crate::traits::Contains; type RuntimeOrigin; type RuntimeCall; + type RuntimeTask; type PalletInfo: crate::traits::PalletInfo; type DbWeight: Get; } diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index d37490c341724..a3aa57b595653 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -849,6 +849,7 @@ pub mod tests { type BaseCallFilter: crate::traits::Contains; type RuntimeOrigin; type RuntimeCall; + type RuntimeTask; type PalletInfo: crate::traits::PalletInfo; type DbWeight: Get; } @@ -1551,8 +1552,9 @@ pub mod pallet_prelude { StorageList, }, traits::{ - BuildGenesisConfig, ConstU32, EnsureOrigin, Get, GetDefault, GetStorageVersion, Hooks, - IsType, PalletInfoAccess, StorageInfoTrait, StorageVersion, TypedGet, + AggregatedTask, BuildGenesisConfig, ConstU32, EnsureOrigin, Get, GetDefault, + GetStorageVersion, Hooks, IsType, PalletInfoAccess, StorageInfoTrait, StorageVersion, + Task, TypedGet, }, Blake2_128, Blake2_128Concat, Blake2_256, CloneNoBound, DebugNoBound, EqNoBound, Identity, PartialEqNoBound, RuntimeDebug, RuntimeDebugNoBound, Twox128, Twox256, Twox64Concat, diff --git a/frame/support/src/storage/generator/mod.rs b/frame/support/src/storage/generator/mod.rs index bac9f642e37d6..7ea98b9635bbc 100644 --- a/frame/support/src/storage/generator/mod.rs +++ b/frame/support/src/storage/generator/mod.rs @@ -63,6 +63,7 @@ mod tests { type BaseCallFilter: crate::traits::Contains; type RuntimeOrigin; type RuntimeCall; + type RuntimeTask; type PalletInfo: crate::traits::PalletInfo; type DbWeight: Get; } diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 27d16e133aa02..2d18648ce5384 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -116,6 +116,9 @@ pub use messages::{ TransformOrigin, }; +pub mod tasks; +pub use tasks::{AggregatedTask, Task}; + #[cfg(feature = "try-runtime")] mod try_runtime; #[cfg(feature = "try-runtime")] diff --git a/frame/support/src/traits/tasks.rs b/frame/support/src/traits/tasks.rs new file mode 100644 index 0000000000000..c52fc7d179179 --- /dev/null +++ b/frame/support/src/traits/tasks.rs @@ -0,0 +1,74 @@ +// This file is part of Substrate. + +// 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. + +//! Contains the [`Task`] trait, which defines a general-purpose way for defining and executing +//! service work, and supporting types. + +/// Contain's re-exports of all the supporting types for the [`Task`] trait. Used in the macro +/// expansion of `RuntimeTask`. +pub mod prelude { + pub use codec::FullCodec; + pub use scale_info::TypeInfo; + pub use sp_runtime::DispatchError; + pub use sp_std::{fmt::Debug, iter::Iterator}; + pub use sp_weights::Weight; +} + +use prelude::*; + +/// A general-purpose trait which defines a type of service work (i.e., work to performed by an +/// off-chain worker) including methods for enumerating, validating, indexing, and running +/// tasks of this type. +pub trait Task: Sized + FullCodec + TypeInfo + Clone + Debug + PartialEq + Eq { + type Enumeration: Iterator; + + /// A unique value representing this `Task`. Analogous to `call_index`, but for tasks. + const TASK_INDEX: u64; + + /// Inspects the pallet's state and enumerates tasks of this type. + fn enumerate() -> Self::Enumeration; + + /// Checks if a particular instance of this `Task` variant is a valid piece of work. + fn is_valid(&self) -> bool; + + /// Performs the work for this particular `Task` variant. + fn run(&self) -> Result<(), DispatchError>; + + /// Returns the weight of executing this `Task`. + fn weight(&self) -> Weight; + + /// A unique value representing this `Task`. Analogous to `call_index`, but for tasks. + fn task_index(&self) -> u64 { + Self::TASK_INDEX + } +} + +/// Contains a subset of [`Task`] that can be generalized over the aggregated `RuntimeTask` +/// enum. +pub trait AggregatedTask: Sized + FullCodec + TypeInfo + Clone + Debug + PartialEq + Eq { + /// Checks if a particular instance of this `Task` variant is a valid piece of work. + fn is_valid(&self) -> bool; + + /// Performs the work for this particular `Task` variant. + fn run(&self) -> Result<(), DispatchError>; + + /// Returns the weight of executing this `Task`. + fn weight(&self) -> Weight; + + /// A unique value representing this `Task`. Analogous to `call_index`, but for tasks. + fn task_index(&self) -> u64; +} diff --git a/frame/support/test/src/lib.rs b/frame/support/test/src/lib.rs index 6b38d42d33d0d..a8a723375033a 100644 --- a/frame/support/test/src/lib.rs +++ b/frame/support/test/src/lib.rs @@ -50,6 +50,8 @@ pub mod pallet { + From>; /// The runtime call type. type RuntimeCall; + /// Contains an aggregation of all tasks in this runtime. + type RuntimeTask; /// The runtime event type. type RuntimeEvent: Parameter + Member diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index 56006269ab1df..8aa2bbd00f25a 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -201,7 +201,7 @@ impl, MaxOverflow: Get> ConsumerLimits for (MaxNormal, #[frame_support::pallet] pub mod pallet { use crate::{self as frame_system, pallet_prelude::*, *}; - use frame_support::pallet_prelude::*; + use frame_support::{pallet_prelude::*, traits::AggregatedTask}; /// Contains default types suitable for various environments pub mod config_preludes { @@ -273,6 +273,10 @@ pub mod pallet { + Debug + From>; + /// The aggregated `RuntimeTask` type. + #[pallet::no_default] + type RuntimeTask: AggregatedTask; + /// This stores the number of previous transactions associated with a sender account. type Nonce: Parameter + Member @@ -500,6 +504,28 @@ pub mod pallet { Self::deposit_event(Event::Remarked { sender: who, hash }); Ok(().into()) } + + #[pallet::call_index(8)] + #[pallet::weight(task.weight())] + pub fn do_task(origin: OriginFor, task: T::RuntimeTask) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + if !task.is_valid() { + return Err(Error::::InvalidTask.into()) + } + + Self::deposit_event(Event::TaskStarted { task: task.clone() }); + if let Err(err) = task.run() { + Self::deposit_event(Event::TaskFailed { task, err }); + return Err(Error::::FailedTask.into()) + } + + // Emit a success event, if your design includes events for this pallet. + Self::deposit_event(Event::TaskCompleted { task }); + + // Return success. + Ok(().into()) + } } /// Event for the System pallet. @@ -517,6 +543,12 @@ pub mod pallet { KilledAccount { account: T::AccountId }, /// On on-chain remark happened. Remarked { sender: T::AccountId, hash: T::Hash }, + /// A [`Task`] has started executing + TaskStarted { task: T::RuntimeTask }, + /// A [`Task`] has finished executing. + TaskCompleted { task: T::RuntimeTask }, + /// A [`Task`] failed during execution. + TaskFailed { task: T::RuntimeTask, err: DispatchError }, } /// Error for the System pallet @@ -538,6 +570,10 @@ pub mod pallet { NonZeroRefCount, /// The origin filter prevent the call to be dispatched. CallFiltered, + /// The specified [`Task`] is not valid. + InvalidTask, + /// The specified [`Task`] failed during execution. + FailedTask, } /// Exposed trait-generic origin type. diff --git a/frame/system/src/mock.rs b/frame/system/src/mock.rs index c016ea9e1cd14..670dbb266b036 100644 --- a/frame/system/src/mock.rs +++ b/frame/system/src/mock.rs @@ -91,6 +91,7 @@ impl Config for Test { type BlockLength = RuntimeBlockLength; type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; type Nonce = u64; type Hash = H256; type Hashing = BlakeTwo256; diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index 0cc32e50956c8..4bac6a86b9c31 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -349,6 +349,7 @@ impl frame_system::pallet::Config for Runtime { type BlockLength = (); type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; type Nonce = Nonce; type Hash = H256; type Hashing = Hashing;