diff --git a/Cargo.lock b/Cargo.lock index 23a5fb7926925..db2ad43d459d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1289,6 +1289,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -2028,6 +2034,33 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "docify" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b972b74c30cbe838fc6a07665132ff94f257350e26fd01d80bc59ee7fcf129" +dependencies = [ + "docify_macros", +] + +[[package]] +name = "docify_macros" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c93004d1011191c56df9e853dca42f2012e7488638bcd5078935f5ce43e06cf3" +dependencies = [ + "common-path", + "derive-syn-parse", + "lazy_static", + "prettyplease 0.2.4", + "proc-macro2", + "quote", + "regex", + "syn 2.0.16", + "termcolor", + "walkdir", +] + [[package]] name = "downcast" version = "0.11.0" @@ -6418,6 +6451,7 @@ dependencies = [ name = "pallet-fast-unstake" version = "4.0.0-dev" dependencies = [ + "docify", "frame-benchmarking", "frame-election-provider-support", "frame-support", diff --git a/frame/fast-unstake/Cargo.toml b/frame/fast-unstake/Cargo.toml index fc438a22467f5..736a67f6e81b0 100644 --- a/frame/fast-unstake/Cargo.toml +++ b/frame/fast-unstake/Cargo.toml @@ -27,6 +27,8 @@ frame-election-provider-support = { default-features = false, path = "../electio frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +docify = "0.1.13" + [dev-dependencies] pallet-staking-reward-curve = { version = "4.0.0-dev", path = "../staking/reward-curve" } sp-core = { version = "8.0.0", default-features = false, path = "../../primitives/core" } diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs index f8a8e00346a34..9be5878f2e199 100644 --- a/frame/fast-unstake/src/lib.rs +++ b/frame/fast-unstake/src/lib.rs @@ -15,37 +15,100 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! A pallet that's designed to JUST do the following: +//! > Made with *Substrate*, for *Polkadot*. //! -//! If a nominator is not exposed in any `ErasStakers` (i.e. "has not actively backed any -//! validators in the last `BondingDuration` days"), then they can register themselves in this -//! pallet, unstake faster than having to wait an entire bonding duration. +//! [![github]](https://github.com/paritytech/substrate/frame/fast-unstake) - +//! [![polkadot]](https://polkadot.network) //! -//! Appearing in the exposure of a validator means being exposed equal to that validator from the -//! point of view of the staking system. This usually means earning rewards with the validator, and -//! also being at the risk of slashing with the validator. This is equivalent to the "Active -//! Nominator" role explained in the -//! [February Staking Update](https://polkadot.network/blog/staking-update-february-2022/). +//! [polkadot]: 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 //! -//! This pallet works off the basis of `on_idle`, meaning that it provides no guarantee about when -//! it will succeed, if at all. Moreover, the queue implementation is unordered. In case of -//! congestion, no FIFO ordering is provided. +//! # Fast Unstake Pallet +//! +//! A pallet to allow participants of the staking system (represented by [`Config::Staking`], being +//! [`StakingInterface`]) to unstake quicker, if and only if they meet the condition of not being +//! exposed to any slashes. +//! +//! ## Overview +//! +//! If a nominator is not exposed anywhere in the staking system, checked via +//! [`StakingInterface::is_exposed_in_era`] (i.e. "has not actively backed any validators in the +//! last [`StakingInterface::bonding_duration`] days"), then they can register themselves in this +//! pallet and unstake faster than having to wait an entire bonding duration. +//! +//! *Being exposed with validator* from the point of view of the staking system means earning +//! rewards with the validator, and also being at the risk of slashing with the validator. This is +//! equivalent to the "Active Nominator" role explained in +//! [here](https://polkadot.network/blog/staking-update-february-2022/). //! //! Stakers who are certain about NOT being exposed can register themselves with -//! [`Call::register_fast_unstake`]. This will chill, and fully unbond the staker, and place them in -//! the queue to be checked. +//! [`Pallet::register_fast_unstake`]. This will chill, fully unbond the staker and place them +//! in the queue to be checked. //! -//! Once queued, but not being actively processed, stakers can withdraw their request via -//! [`Call::deregister`]. +//! A successful registration implies being fully unbonded and chilled in the staking system. These +//! effects persist even if the fast-unstake registration is retracted (see [`Pallet::deregister`] +//! and further). //! -//! Once queued, a staker wishing to unbond can perform no further action in pallet-staking. This is -//! to prevent them from accidentally exposing themselves behind a validator etc. +//! Once registered as a fast-unstaker, the staker will be queued and checked by the system. This +//! can take a variable number of blocks based on demand, but will almost certainly be "faster" (as +//! the name suggest) than waiting the standard bonding duration. +//! +//! A fast-unstaker is either in [`Queue`] or actively being checked, at which point it lives in +//! [`Head`]. Once in [`Head`], the request cannot be retracted anymore. But, once in [`Queue`], it +//! can, via [`Pallet::deregister`]. +//! +//! A deposit equal to [`Config::Deposit`] is collected for this process, and is returned in case a +//! successful unstake occurs (`Event::Unstaked` signals that). //! //! Once processed, if successful, no additional fee for the checking process is taken, and the //! staker is instantly unbonded. //! -//! If unsuccessful, meaning that the staker was exposed sometime in the last `BondingDuration` eras -//! they will end up being slashed for the amount of wasted work they have inflicted on the chian. +//! If unsuccessful, meaning that the staker was exposed, the aforementioned deposit will be slashed +//! for the amount of wasted work they have inflicted on the chain. +//! +//! All in all, this pallet is meant to provide an easy off-ramp for some stakers. +//! +//! ### Example +//! +//! 1. Fast-unstake with multiple participants in the queue. +#![doc = docify::embed!("frame/fast-unstake/src/tests.rs", successful_multi_queue)] +//! +//! 2. Fast unstake failing because a nominator is exposed. +#![doc = docify::embed!("frame/fast-unstake/src/tests.rs", exposed_nominator_cannot_unstake)] +//! +//! ## Pallet API +//! +//! See the [`pallet`] module for more information about the interfaces this pallet exposes, +//! including its configuration trait, dispatchables, storage items, events and errors. +//! +//! ## Low Level / Implementation Details +//! +//! This pallet works off the basis of `on_idle`, meaning that it provides no guarantee about when +//! it will succeed, if at all. Moreover, the queue implementation is unordered. In case of +//! congestion, no FIFO ordering is provided. +//! +//! A few important considerations can be concluded based on the `on_idle`-based implementation: +//! +//! * It is crucial for the weights of this pallet to be correct. The code inside +//! [`Pallet::on_idle`] MUST be able to measure itself and report the remaining weight correctly +//! after execution. +//! +//! * If the weight measurement is incorrect, it can lead to perpetual overweight (consequently +//! slow) blocks. +//! +//! * The amount of weight that `on_idle` consumes is a direct function of [`ErasToCheckPerBlock`]. +//! +//! * Thus, a correct value of [`ErasToCheckPerBlock`] (which can be set via [`Pallet::control`]) +//! should be chosen, such that a reasonable amount of weight is used `on_idle`. If +//! [`ErasToCheckPerBlock`] is too large, `on_idle` will always conclude that it has not enough +//! weight to proceed, and will early-return. Nonetheless, this should also be *safe* as long as +//! the benchmarking/weights are *accurate*. +//! +//! * See the inline code-comments on `do_on_idle` (private) for more details. +//! +//! * For further safety, in case of any unforeseen errors, the pallet will emit +//! [`Event::InternalError`] and set [`ErasToCheckPerBlock`] back to 0, which essentially means +//! the pallet will halt/disable itself. #![cfg_attr(not(feature = "std"), no_std)] @@ -64,9 +127,15 @@ pub mod migrations; pub mod types; pub mod weights; +// some extra imports for docs to link properly. +#[cfg(doc)] +pub use frame_support::traits::Hooks; +#[cfg(doc)] +pub use sp_staking::StakingInterface; + +/// The logging target of this pallet. pub const LOG_TARGET: &'static str = "runtime::fast-unstake"; -// syntactic sugar for logging. #[macro_export] macro_rules! log { ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { @@ -94,16 +163,6 @@ pub mod pallet { #[cfg(feature = "try-runtime")] use sp_runtime::TryRuntimeError; - #[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)] - #[codec(mel_bound(T: Config))] - #[scale_info(skip_type_params(T))] - pub struct MaxChecking(sp_std::marker::PhantomData); - impl frame_support::traits::Get for MaxChecking { - fn get() -> u32 { - T::Staking::bonding_duration() + 1 - } - } - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); #[pallet::pallet] @@ -125,7 +184,7 @@ pub mod pallet { #[pallet::constant] type Deposit: Get>; - /// The origin that can control this pallet. + /// The origin that can control this pallet, in other words invoke [`Pallet::control`]. type ControlOrigin: frame_support::traits::EnsureOrigin; /// Batch size. @@ -136,40 +195,45 @@ pub mod pallet { /// The access to staking functionality. type Staking: StakingInterface, AccountId = Self::AccountId>; + /// Maximum value for `ErasToCheckPerBlock`, checked in [`Pallet::control`]. + /// + /// This should be slightly bigger than the actual value in order to have accurate + /// benchmarks. + type MaxErasToCheckPerBlock: Get; + /// The weight information of this pallet. type WeightInfo: WeightInfo; - /// Maximum value for `ErasToCheckPerBlock`. This should be as close as possible, but more - /// than the actual value, in order to have accurate benchmarks. - type MaxErasToCheckPerBlock: Get; - /// Use only for benchmarking. #[cfg(feature = "runtime-benchmarks")] type MaxBackersPerValidator: Get; } /// The current "head of the queue" being unstaked. + /// + /// The head in itself can be a batch of up to [`Config::BatchSize`] stakers. #[pallet::storage] pub type Head = StorageValue<_, UnstakeRequest, OptionQuery>; /// The map of all accounts wishing to be unstaked. /// /// Keeps track of `AccountId` wishing to unstake and it's corresponding deposit. - /// - /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. + // Hasher: Twox safe since `AccountId` is a secure hash. #[pallet::storage] pub type Queue = CountedStorageMap<_, Twox64Concat, T::AccountId, BalanceOf>; /// Number of eras to check per block. /// - /// If set to 0, this pallet does absolutely nothing. + /// If set to 0, this pallet does absolutely nothing. Cannot be set to more than + /// [`Config::MaxErasToCheckPerBlock`]. /// - /// Based on the amount of weight available at `on_idle`, up to this many eras of a single - /// nominator might be checked. + /// Based on the amount of weight available at [`Pallet::on_idle`], up to this many eras are + /// checked. The checking is represented by updating [`UnstakeRequest::checked`], which is + /// stored in [`Head`]. #[pallet::storage] + #[pallet::getter(fn eras_to_check_per_block)] pub type ErasToCheckPerBlock = StorageValue<_, u32, ValueQuery>; - /// The events of this pallet. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -177,8 +241,6 @@ pub mod pallet { Unstaked { stash: T::AccountId, result: DispatchResult }, /// A staker was slashed for requesting fast-unstake whilst being exposed. Slashed { stash: T::AccountId, amount: BalanceOf }, - /// An internal error happened. Operations will be paused now. - InternalError, /// A batch was partially checked for the given eras, but the process did not finish. BatchChecked { eras: Vec }, /// A batch of a given size was terminated. @@ -186,6 +248,8 @@ pub mod pallet { /// This is always follows by a number of `Unstaked` or `Slashed` events, marking the end /// of the batch. A new batch will be created upon next block. BatchFinished { size: u32 }, + /// An internal error happened. Operations will be paused now. + InternalError, } #[pallet::error] @@ -247,8 +311,12 @@ pub mod pallet { impl Pallet { /// Register oneself for fast-unstake. /// - /// The dispatch origin of this call must be signed by the controller account, similar to - /// `staking::unbond`. + /// ## Dispatch Origin + /// + /// The dispatch origin of this call must be *signed* by whoever is permitted to call + /// unbond funds by the staking system. See [`Config::Staking`]. + /// + /// ## Details /// /// The stash associated with the origin must have no ongoing unlocking chunks. If /// successful, this will fully unbond and chill the stash. Then, it will enqueue the stash @@ -263,6 +331,10 @@ pub mod pallet { /// If the check fails, the stash remains chilled and waiting for being unbonded as in with /// the normal staking system, but they lose part of their unbonding chunks due to consuming /// the chain's resources. + /// + /// ## Events + /// + /// Some events from the staking and currency system might be emitted. #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::register_fast_unstake())] pub fn register_fast_unstake(origin: OriginFor) -> DispatchResult { @@ -288,11 +360,22 @@ pub mod pallet { /// Deregister oneself from the fast-unstake. /// + /// ## Dispatch Origin + /// + /// The dispatch origin of this call must be *signed* by whoever is permitted to call + /// unbond funds by the staking system. See [`Config::Staking`]. + /// + /// ## Details + /// /// This is useful if one is registered, they are still waiting, and they change their mind. /// /// Note that the associated stash is still fully unbonded and chilled as a consequence of - /// calling `register_fast_unstake`. This should probably be followed by a call to - /// `Staking::rebond`. + /// calling [`Pallet::register_fast_unstake`]. Therefore, this should probably be followed + /// by a call to `rebond` in the staking system. + /// + /// ## Events + /// + /// Some events from the staking and currency system might be emitted. #[pallet::call_index(1)] #[pallet::weight(::WeightInfo::deregister())] pub fn deregister(origin: OriginFor) -> DispatchResult { @@ -318,7 +401,17 @@ pub mod pallet { /// Control the operation of this pallet. /// - /// Dispatch origin must be signed by the [`Config::ControlOrigin`]. + /// ## Dispatch Origin + /// + /// The dispatch origin of this call must be [`Config::ControlOrigin`]. + /// + /// ## Details + /// + /// Can set the number of eras to check per block, and potentially other admin work. + /// + /// ## Events + /// + /// No events are emitted from this dispatch. #[pallet::call_index(2)] #[pallet::weight(::WeightInfo::control())] pub fn control(origin: OriginFor, eras_to_check: EraIndex) -> DispatchResult { diff --git a/frame/fast-unstake/src/tests.rs b/frame/fast-unstake/src/tests.rs index c51c817ec6a74..94ad6a84b85a1 100644 --- a/frame/fast-unstake/src/tests.rs +++ b/frame/fast-unstake/src/tests.rs @@ -19,7 +19,7 @@ use super::*; use crate::{mock::*, types::*, Event}; -use frame_support::{assert_noop, assert_ok, bounded_vec, pallet_prelude::*, traits::Currency}; +use frame_support::{pallet_prelude::*, testing_prelude::*, traits::Currency}; use pallet_staking::{CurrentEra, RewardDestination}; use sp_runtime::traits::BadOrigin; @@ -303,6 +303,7 @@ mod on_idle { }); } + #[docify::export] #[test] fn successful_multi_queue() { ExtBuilder::default().build_and_execute(|| { @@ -356,6 +357,7 @@ mod on_idle { }); } + #[docify::export] #[test] fn successful_unstake() { ExtBuilder::default().build_and_execute(|| { @@ -693,6 +695,7 @@ mod on_idle { }); } + #[docify::export] #[test] fn exposed_nominator_cannot_unstake() { ExtBuilder::default().build_and_execute(|| { diff --git a/frame/fast-unstake/src/types.rs b/frame/fast-unstake/src/types.rs index 3ec4b3a9b4d6e..15d0a327e917e 100644 --- a/frame/fast-unstake/src/types.rs +++ b/frame/fast-unstake/src/types.rs @@ -17,25 +17,41 @@ //! Types used in the Fast Unstake pallet. -use crate::{Config, MaxChecking}; +use crate::Config; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ traits::Currency, BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use scale_info::TypeInfo; -use sp_staking::EraIndex; +use sp_staking::{EraIndex, StakingInterface}; use sp_std::prelude::*; -pub type BalanceOf = +/// Maximum number of eras that we might check for a single staker. +/// +/// In effect, it is the bonding duration, coming from [`Config::Staking`], plus one. +#[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct MaxChecking(sp_std::marker::PhantomData); +impl frame_support::traits::Get for MaxChecking { + fn get() -> u32 { + T::Staking::bonding_duration() + 1 + } +} + +pub(crate) type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; /// An unstake request. +/// +/// This is stored in [`crate::Head`] storage item and points to the current unstake request that is +/// being processed. #[derive( Encode, Decode, EqNoBound, PartialEqNoBound, Clone, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen, )] #[scale_info(skip_type_params(T))] pub struct UnstakeRequest { - /// This list of stashes being processed in this request, and their corresponding deposit. - pub(crate) stashes: BoundedVec<(T::AccountId, BalanceOf), T::BatchSize>, + /// This list of stashes are being processed in this request, and their corresponding deposit. + pub stashes: BoundedVec<(T::AccountId, BalanceOf), T::BatchSize>, /// The list of eras for which they have been checked. - pub(crate) checked: BoundedVec>, + pub checked: BoundedVec>, } diff --git a/frame/support/procedural/src/pallet/expand/call.rs b/frame/support/procedural/src/pallet/expand/call.rs index f17fdc81a647c..e7b80faa0b7ed 100644 --- a/frame/support/procedural/src/pallet/expand/call.rs +++ b/frame/support/procedural/src/pallet/expand/call.rs @@ -113,7 +113,13 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream { } debug_assert_eq!(fn_weight.len(), methods.len()); - let fn_doc = methods.iter().map(|method| &method.docs).collect::>(); + let fn_doc = methods + .iter() + .map(|method| { + let reference = format!("See [`Pallet::{}`].", method.name); + quote!(#reference) + }) + .collect::>(); let args_name = methods .iter() @@ -175,9 +181,8 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream { .collect::>() }); - let default_docs = [syn::parse_quote!( - r"Contains one variant per dispatchable that can be called by an extrinsic." - )]; + let default_docs = + [syn::parse_quote!(r"Contains a variant per dispatchable extrinsic that this pallet has.")]; let docs = if docs.is_empty() { &default_docs[..] } else { &docs[..] }; let maybe_compile_error = if def.call.is_none() { @@ -274,7 +279,7 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream { #frame_support::Never, ), #( - #( #[doc = #fn_doc] )* + #[doc = #fn_doc] #[codec(index = #call_index)] #fn_name { #( diff --git a/frame/support/procedural/src/pallet/expand/config.rs b/frame/support/procedural/src/pallet/expand/config.rs index c70f6eb80422a..dd7471aa767cb 100644 --- a/frame/support/procedural/src/pallet/expand/config.rs +++ b/frame/support/procedural/src/pallet/expand/config.rs @@ -16,7 +16,6 @@ // limitations under the License. use crate::pallet::Def; -use frame_support_procedural_tools::get_doc_literals; /// /// * Generate default rust doc @@ -31,15 +30,19 @@ pub fn expand_config(def: &mut Def) -> proc_macro2::TokenStream { } }; - if get_doc_literals(&config_item.attrs).is_empty() { - config_item.attrs.push(syn::parse_quote!( - #[doc = r" - Configuration trait of this pallet. + config_item.attrs.insert( + 0, + syn::parse_quote!( + #[doc = r"Configuration trait of this pallet. - Implement this type for a runtime in order to customize this pallet. - "] - )); - } +The main purpose of this trait is to act as an interface between this pallet and the runtime in +which it is embedded in. A type, function, or constant in this trait is essentially left to be +configured by the runtime that includes this pallet. + +Consequently, a runtime that wants to include this pallet must implement this trait." + ] + ), + ); Default::default() } diff --git a/frame/support/procedural/src/pallet/expand/doc_only.rs b/frame/support/procedural/src/pallet/expand/doc_only.rs index 32c9329f29498..50afeb3ca88cf 100644 --- a/frame/support/procedural/src/pallet/expand/doc_only.rs +++ b/frame/support/procedural/src/pallet/expand/doc_only.rs @@ -20,8 +20,6 @@ use proc_macro2::Span; use crate::pallet::Def; pub fn expand_doc_only(def: &mut Def) -> proc_macro2::TokenStream { - let storage_names = def.storages.iter().map(|storage| &storage.ident); - let storage_docs = def.storages.iter().map(|storage| &storage.docs); let dispatchables = if let Some(call_def) = &def.call { let type_impl_generics = def.type_impl_generics(Span::call_site()); call_def @@ -35,17 +33,16 @@ pub fn expand_doc_only(def: &mut Def) -> proc_macro2::TokenStream { .map(|(_, arg_name, arg_type)| quote::quote!( #arg_name: #arg_type, )) .collect::(); let docs = &method.docs; - let line_2 = - format!(" designed to document the [`{}`][`Call::{}`] variant of", name, name); + + let real = format!(" [`Pallet::{}`].", name); quote::quote!( #( #[doc = #docs] )* /// - /// --- + /// # Warning: Doc-Only /// - /// NOTE: This function is an automatically generated, doc only, uncallable stub. - #[ doc = #line_2 ] - /// the pallet [`Call`] enum. You should not attempt to call this function - /// directly. + /// This function is an automatically generated, and is doc-only, uncallable + /// stub. See the real version in + #[ doc = #real ] pub fn #name<#type_impl_generics>(#args) { unreachable!(); } ) }) @@ -54,22 +51,49 @@ pub fn expand_doc_only(def: &mut Def) -> proc_macro2::TokenStream { quote::quote!() }; + let storage_types = def + .storages + .iter() + .map(|storage| { + let storage_name = &storage.ident; + let storage_type_docs = &storage.docs; + let real = format!("[`pallet::{}`].", storage_name); + quote::quote!( + #( #[doc = #storage_type_docs] )* + /// + /// # Warning: Doc-Only + /// + /// This type is automatically generated, and is doc-only. See the real version in + #[ doc = #real ] + pub struct #storage_name(); + ) + }) + .collect::(); + quote::quote!( - /// Auto-generated docs-only module listing all defined storage types for this pallet. - /// Note that members of this module cannot be used directly and are only provided for - /// documentation purposes. + /// Auto-generated docs-only module listing all (public and private) defined storage types + /// for this pallet. + /// + /// # Warning: Doc-Only + /// + /// Members of this module cannot be used directly and are only provided for documentation + /// purposes. + /// + /// To see the actual storage type, find a struct with the same name at the root of the + /// pallet, in the list of [*Type Definitions*](../index.html#types). #[cfg(doc)] pub mod storage_types { use super::*; - #( - #( #[doc = #storage_docs] )* - pub struct #storage_names(); - )* + #storage_types } /// Auto-generated docs-only module listing all defined dispatchables for this pallet. - /// Note that members of this module cannot be used directly and are only provided for - /// documentation purposes. + /// + /// # Warning: Doc-Only + /// + /// Members of this module cannot be used directly and are only provided for documentation + /// purposes. To see the real version of each dispatchable, look for them in [`Pallet`] or + /// [`Call`]. #[cfg(doc)] pub mod dispatchables { use super::*; diff --git a/frame/support/procedural/src/pallet/expand/error.rs b/frame/support/procedural/src/pallet/expand/error.rs index 70f9fdfc71112..376a6a9f51c6d 100644 --- a/frame/support/procedural/src/pallet/expand/error.rs +++ b/frame/support/procedural/src/pallet/expand/error.rs @@ -110,10 +110,7 @@ pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream { if get_doc_literals(&error_item.attrs).is_empty() { error_item.attrs.push(syn::parse_quote!( - #[doc = r" - Custom [dispatch errors](https://docs.substrate.io/main-docs/build/events-errors/) - of this pallet. - "] + #[doc = "The `Error` enum of this pallet."] )); } diff --git a/frame/support/procedural/src/pallet/expand/event.rs b/frame/support/procedural/src/pallet/expand/event.rs index 2f0cefb8b9fc3..f94bdef332d9d 100644 --- a/frame/support/procedural/src/pallet/expand/event.rs +++ b/frame/support/procedural/src/pallet/expand/event.rs @@ -97,12 +97,9 @@ pub fn expand_event(def: &mut Def) -> proc_macro2::TokenStream { } if get_doc_literals(&event_item.attrs).is_empty() { - event_item.attrs.push(syn::parse_quote!( - #[doc = r" - The [event](https://docs.substrate.io/main-docs/build/events-errors/) emitted - by this pallet. - "] - )); + event_item + .attrs + .push(syn::parse_quote!(#[doc = "The `Event` enum of this pallet"])); } // derive some traits because system event require Clone, FullCodec, Eq, PartialEq and Debug diff --git a/frame/support/procedural/src/pallet/expand/mod.rs b/frame/support/procedural/src/pallet/expand/mod.rs index 926ab0ec82d73..2b998227c1d84 100644 --- a/frame/support/procedural/src/pallet/expand/mod.rs +++ b/frame/support/procedural/src/pallet/expand/mod.rs @@ -36,7 +36,6 @@ mod type_value; mod validate_unsigned; use crate::pallet::Def; -use frame_support_procedural_tools::get_doc_literals; use quote::ToTokens; /// Merge where clause together, `where` token span is taken from the first not none one. @@ -75,16 +74,24 @@ pub fn expand(mut def: Def) -> proc_macro2::TokenStream { let tt_default_parts = tt_default_parts::expand_tt_default_parts(&mut def); let doc_only = doc_only::expand_doc_only(&mut def); - if get_doc_literals(&def.item.attrs).is_empty() { - def.item.attrs.push(syn::parse_quote!( - #[doc = r" - The module that hosts all the - [FRAME](https://docs.substrate.io/main-docs/build/events-errors/) - types needed to add this pallet to a - runtime. - "] - )); - } + def.item.attrs.insert( + 0, + syn::parse_quote!( + #[doc = r"The `pallet` module in each FRAME pallet hosts the most important items needed +to construct this pallet. + +The main components of this pallet are: +- [`Pallet`], which implements all of the dispatchable extrinsics of the pallet, among +other public functions. + - The subset of the functions that are dispatchable can be identified either in the + [`dispatchables`] module or in the [`Call`] enum. +- [`storage_types`], which contains the list of all types that are representing a +storage item. Otherwise, all storage items are listed among [*Type Definitions*](#types). +- [`Config`], which contains the configuration trait of this pallet. +- [`Event`] and [`Error`], which are listed among the [*Enums*](#enums). + "] + ), + ); let new_items = quote::quote!( #metadata_docs diff --git a/frame/support/procedural/src/pallet/expand/pallet_struct.rs b/frame/support/procedural/src/pallet/expand/pallet_struct.rs index 99d2d79f231d9..800e23388c1af 100644 --- a/frame/support/procedural/src/pallet/expand/pallet_struct.rs +++ b/frame/support/procedural/src/pallet/expand/pallet_struct.rs @@ -62,8 +62,8 @@ pub fn expand_pallet_struct(def: &mut Def) -> proc_macro2::TokenStream { if get_doc_literals(&pallet_item.attrs).is_empty() { pallet_item.attrs.push(syn::parse_quote!( #[doc = r" - The [pallet](https://docs.substrate.io/reference/frame-pallets/#pallets) implementing - the on-chain logic. + The `Pallet` struct, the main type that implements traits and standalone + functions within the pallet. "] )); } diff --git a/frame/support/procedural/src/pallet/expand/storage.rs b/frame/support/procedural/src/pallet/expand/storage.rs index c742ddcd25fbc..253b429bb6eb3 100644 --- a/frame/support/procedural/src/pallet/expand/storage.rs +++ b/frame/support/procedural/src/pallet/expand/storage.rs @@ -22,6 +22,7 @@ use crate::{ Def, }, }; +use itertools::Itertools; use quote::ToTokens; use std::{collections::HashMap, ops::IndexMut}; use syn::spanned::Spanned; @@ -310,6 +311,65 @@ pub fn process_generics(def: &mut Def) -> syn::Result t, + _ => unreachable!("Checked by def"), + }; + typ_item.attrs.push(syn::parse_quote!(#[doc = ""])); + typ_item.attrs.push(syn::parse_quote!(#[doc = #doc_line])); + }; + def.storages.iter_mut().for_each(|storage| match &storage.metadata { + Metadata::Value { value } => { + let doc_line = format!( + "Storage type is [`StorageValue`] with value type `{}`.", + value.to_token_stream() + ); + push_string_literal(&doc_line, storage); + }, + Metadata::Map { key, value } => { + let doc_line = format!( + "Storage type is [`StorageMap`] with key type `{}` and value type `{}`.", + key.to_token_stream(), + value.to_token_stream() + ); + push_string_literal(&doc_line, storage); + }, + Metadata::DoubleMap { key1, key2, value } => { + let doc_line = format!( + "Storage type is [`StorageDoubleMap`] with key1 type {}, key2 type {} and value type {}.", + key1.to_token_stream(), + key2.to_token_stream(), + value.to_token_stream() + ); + push_string_literal(&doc_line, storage); + }, + Metadata::NMap { keys, value, .. } => { + let doc_line = format!( + "Storage type is [`StorageNMap`] with keys type ({}) and value type {}.", + keys.iter() + .map(|k| k.to_token_stream().to_string()) + .collect::>() + .join(", "), + value.to_token_stream() + ); + push_string_literal(&doc_line, storage); + }, + Metadata::CountedMap { key, value } => { + let doc_line = format!( + "Storage type is [`CountedStorageMap`] with key type {} and value type {}.", + key.to_token_stream(), + value.to_token_stream() + ); + push_string_literal(&doc_line, storage); + }, + }); +} + /// /// * generate StoragePrefix structs (e.g. for a storage `MyStorage` a struct with the name /// `_GeneratedPrefixForStorage$NameOfStorage` is generated) and implements StorageInstance trait. @@ -323,6 +383,8 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { Err(e) => return e.into_compile_error(), }; + augment_final_docs(def); + // Check for duplicate prefixes let mut prefix_set = HashMap::new(); let mut errors = def @@ -365,10 +427,6 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { if let Some(getter) = &storage.getter { let completed_where_clause = super::merge_where_clauses(&[&storage.where_clause, &def.config.where_clause]); - let docs = storage - .docs - .iter() - .map(|d| quote::quote_spanned!(storage.attr_span => #[doc = #d])); let ident = &storage.ident; let gen = &def.type_use_generics(storage.attr_span); @@ -378,6 +436,13 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { let cfg_attrs = &storage.cfg_attrs; + // If the storage item is public, just link to it rather than copy-pasting the docs. + let getter_doc_line = if matches!(storage.vis, syn::Visibility::Public(_)) { + format!("An auto-generated getter for [`{}`].", storage.ident) + } else { + storage.docs.iter().map(|d| d.into_token_stream().to_string()).join("\n") + }; + match &storage.metadata { Metadata::Value { value } => { let query = match storage.query_kind.as_ref().expect("Checked by def") { @@ -394,7 +459,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { quote::quote_spanned!(storage.attr_span => #(#cfg_attrs)* impl<#type_impl_gen> #pallet_ident<#type_use_gen> #completed_where_clause { - #( #docs )* + #[doc = #getter_doc_line] pub fn #getter() -> #query { < #full_ident as #frame_support::storage::StorageValue<#value> @@ -418,7 +483,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { quote::quote_spanned!(storage.attr_span => #(#cfg_attrs)* impl<#type_impl_gen> #pallet_ident<#type_use_gen> #completed_where_clause { - #( #docs )* + #[doc = #getter_doc_line] pub fn #getter(k: KArg) -> #query where KArg: #frame_support::codec::EncodeLike<#key>, { @@ -444,7 +509,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { quote::quote_spanned!(storage.attr_span => #(#cfg_attrs)* impl<#type_impl_gen> #pallet_ident<#type_use_gen> #completed_where_clause { - #( #docs )* + #[doc = #getter_doc_line] pub fn #getter(k: KArg) -> #query where KArg: #frame_support::codec::EncodeLike<#key>, { @@ -470,7 +535,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { quote::quote_spanned!(storage.attr_span => #(#cfg_attrs)* impl<#type_impl_gen> #pallet_ident<#type_use_gen> #completed_where_clause { - #( #docs )* + #[doc = #getter_doc_line] pub fn #getter(k1: KArg1, k2: KArg2) -> #query where KArg1: #frame_support::codec::EncodeLike<#key1>, KArg2: #frame_support::codec::EncodeLike<#key2>, @@ -498,7 +563,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { quote::quote_spanned!(storage.attr_span => #(#cfg_attrs)* impl<#type_impl_gen> #pallet_ident<#type_use_gen> #completed_where_clause { - #( #docs )* + #[doc = #getter_doc_line] pub fn #getter(key: KArg) -> #query where KArg: #frame_support::storage::types::EncodeLikeTuple< diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index d06e6065819cc..80629b9425cef 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -1520,6 +1520,17 @@ pub mod tests { } } +/// Prelude to be used for pallet testing, for ease of use. +#[cfg(feature = "std")] +pub mod testing_prelude { + pub use super::{ + assert_err, assert_err_ignore_postinfo, assert_err_with_weight, assert_error_encoded_size, + assert_noop, assert_ok, assert_storage_noop, bounded_btree_map, bounded_vec, + parameter_types, traits::Get, + }; + pub use sp_arithmetic::assert_eq_error_rate; +} + /// Prelude to be used alongside pallet macro, for ease of use. pub mod pallet_prelude { pub use crate::{ diff --git a/frame/support/test/src/lib.rs b/frame/support/test/src/lib.rs index 5dccc88471a7b..2a3cf13d4dac7 100644 --- a/frame/support/test/src/lib.rs +++ b/frame/support/test/src/lib.rs @@ -34,7 +34,7 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(_); - /// The configuration trait + /// The configuration trait. #[pallet::config] #[pallet::disable_frame_system_supertrait_check] pub trait Config: 'static + Eq + Clone {