diff --git a/Cargo.lock b/Cargo.lock index 84477cd05416..818abc441eca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13385,6 +13385,25 @@ dependencies = [ "sp-runtime 31.0.1", ] +[[package]] +name = "pallet-example-view-functions" +version = "1.0.0" +dependencies = [ + "frame-benchmarking 28.0.0", + "frame-metadata 16.0.0", + "frame-support 28.0.0", + "frame-system 28.0.0", + "log", + "parity-scale-codec", + "pretty_assertions", + "scale-info", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-metadata-ir 0.6.0", + "sp-runtime 31.0.1", + "sp-std 14.0.0", +] + [[package]] name = "pallet-examples" version = "4.0.0-dev" @@ -13399,6 +13418,7 @@ dependencies = [ "pallet-example-single-block-migrations", "pallet-example-split", "pallet-example-tasks", + "pallet-example-view-functions", ] [[package]] @@ -25436,6 +25456,7 @@ dependencies = [ "sp-runtime 31.0.1", "sp-runtime-interface 24.0.0", "sp-state-machine 0.35.0", + "sp-std 14.0.0", "sp-test-primitives", "sp-trie 29.0.0", "sp-version 29.0.0", diff --git a/Cargo.toml b/Cargo.toml index 964964908a9b..a002b1e8474b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -937,6 +937,7 @@ pallet-example-offchain-worker = { path = "substrate/frame/examples/offchain-wor pallet-example-single-block-migrations = { path = "substrate/frame/examples/single-block-migrations", default-features = false } pallet-example-split = { path = "substrate/frame/examples/split", default-features = false } pallet-example-tasks = { path = "substrate/frame/examples/tasks", default-features = false } +pallet-example-view-functions = { path = "substrate/frame/examples/view-functions", default-features = false } pallet-examples = { path = "substrate/frame/examples" } pallet-fast-unstake = { path = "substrate/frame/fast-unstake", default-features = false } pallet-glutton = { path = "substrate/frame/glutton", default-features = false } diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 9f0b701f20be..26f182d6d943 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1595,7 +1595,8 @@ mod runtime { RuntimeHoldReason, RuntimeSlashReason, RuntimeLockId, - RuntimeTask + RuntimeTask, + RuntimeViewFunction )] pub struct Runtime; diff --git a/prdoc/pr_4722.prdoc b/prdoc/pr_4722.prdoc new file mode 100644 index 000000000000..a7aaff5db8cc --- /dev/null +++ b/prdoc/pr_4722.prdoc @@ -0,0 +1,18 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Implement pallet view functions + +doc: + - audience: Runtime Dev + description: | + Read-only view functions can now be defined on pallets. These functions provide an interface for querying state, + from both outside and inside the runtime. Common queries can be defined on pallets, without users having to + access the storage directly. + - audience: Runtime User + description: | + Querying the runtime state is now easier with the introduction of pallet view functions. Clients can call commonly + defined view functions rather than accessing the storage directly. These are similar to the Runtime APIs, but + are defined within the runtime itself. + +crates: [ ] diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index bff263548087..810b2b9122b0 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -101,7 +101,7 @@ use sp_consensus_beefy::{ mmr::MmrLeafVersion, }; use sp_consensus_grandpa::AuthorityId as GrandpaId; -use sp_core::{crypto::KeyTypeId, OpaqueMetadata, H160}; +use sp_core::{crypto::KeyTypeId, OpaqueMetadata, ViewFunctionDispatchError, ViewFunctionId, H160}; use sp_inherents::{CheckInherentsResult, InherentData}; use sp_runtime::{ curve::PiecewiseLinear, @@ -2383,7 +2383,8 @@ mod runtime { RuntimeHoldReason, RuntimeSlashReason, RuntimeLockId, - RuntimeTask + RuntimeTask, + RuntimeViewFunction )] pub struct Runtime; @@ -2925,6 +2926,12 @@ impl_runtime_apis! { } } + impl sp_api::RuntimeViewFunction for Runtime { + fn execute_view_function(id: ViewFunctionId, input: Vec) -> Result, ViewFunctionDispatchError> { + Runtime::execute_view_function(id, input) + } + } + impl sp_block_builder::BlockBuilder for Runtime { fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { Executive::apply_extrinsic(extrinsic) diff --git a/substrate/frame/examples/Cargo.toml b/substrate/frame/examples/Cargo.toml index 0c6ad5ef0978..abdb99dcd70a 100644 --- a/substrate/frame/examples/Cargo.toml +++ b/substrate/frame/examples/Cargo.toml @@ -25,6 +25,7 @@ pallet-example-offchain-worker = { workspace = true } pallet-example-split = { workspace = true } pallet-example-single-block-migrations = { workspace = true } pallet-example-tasks = { workspace = true } +pallet-example-view-functions = { workspace = true } pallet-example-authorization-tx-extension = { workspace = true } [features] @@ -40,6 +41,7 @@ std = [ "pallet-example-single-block-migrations/std", "pallet-example-split/std", "pallet-example-tasks/std", + "pallet-example-view-functions/std", ] try-runtime = [ "pallet-default-config-example/try-runtime", @@ -51,4 +53,5 @@ try-runtime = [ "pallet-example-single-block-migrations/try-runtime", "pallet-example-split/try-runtime", "pallet-example-tasks/try-runtime", + "pallet-example-view-functions/try-runtime", ] diff --git a/substrate/frame/examples/src/lib.rs b/substrate/frame/examples/src/lib.rs index d0d30830f2f0..200e92112a3f 100644 --- a/substrate/frame/examples/src/lib.rs +++ b/substrate/frame/examples/src/lib.rs @@ -48,6 +48,9 @@ //! //! - [`pallet_example_tasks`]: This pallet demonstrates the use of `Tasks` to execute service work. //! +//! - [`pallet_example_view_functions`]: This pallet demonstrates the use of view functions to query +//! pallet state. +//! //! - [`pallet_example_authorization_tx_extension`]: An example `TransactionExtension` that //! authorizes a custom origin through signature validation, along with two support pallets to //! showcase the usage. diff --git a/substrate/frame/examples/view-functions/Cargo.toml b/substrate/frame/examples/view-functions/Cargo.toml new file mode 100644 index 000000000000..d94a0dd5159e --- /dev/null +++ b/substrate/frame/examples/view-functions/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "pallet-example-view-functions" +version = "1.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Pallet to demonstrate the usage of view functions to query pallet state" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false } +log = { workspace = true } +scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } +frame-metadata = { features = ["current"], workspace = true } + +frame-support = { path = "../../support", default-features = false } +frame-system = { path = "../../system", default-features = false } + +sp-io = { path = "../../../primitives/io", default-features = false } +sp-metadata-ir = { path = "../../../primitives/metadata-ir", default-features = false } +sp-runtime = { path = "../../../primitives/runtime", default-features = false } +sp-std = { path = "../../../primitives/std", default-features = false } +sp-core = { default-features = false, path = "../../../primitives/core" } + +frame-benchmarking = { path = "../../benchmarking", default-features = false, optional = true } + +[dev-dependencies] +pretty_assertions = { version = "1.3.0" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] \ No newline at end of file diff --git a/substrate/frame/examples/view-functions/src/lib.rs b/substrate/frame/examples/view-functions/src/lib.rs new file mode 100644 index 000000000000..e842a718ad33 --- /dev/null +++ b/substrate/frame/examples/view-functions/src/lib.rs @@ -0,0 +1,114 @@ +// 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. + +//! This pallet demonstrates the use of the `pallet::view_functions_experimental` api for service +//! work. +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod tests; + +use frame_support::Parameter; +use scale_info::TypeInfo; + +pub struct SomeType1; +impl From for u64 { + fn from(_t: SomeType1) -> Self { + 0u64 + } +} + +pub trait SomeAssociation1 { + type _1: Parameter + codec::MaxEncodedLen + TypeInfo; +} +impl SomeAssociation1 for u64 { + type _1 = u64; +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + + #[pallet::error] + pub enum Error {} + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::storage] + pub type SomeValue = StorageValue<_, u32>; + + #[pallet::storage] + pub type SomeMap = StorageMap<_, Twox64Concat, u32, u32, OptionQuery>; + + #[pallet::view_functions_experimental] + impl Pallet + where + T::AccountId: From + SomeAssociation1, + { + /// Query value no args. + pub fn get_value() -> Option { + SomeValue::::get() + } + + /// Query value with args. + pub fn get_value_with_arg(key: u32) -> Option { + SomeMap::::get(key) + } + } +} + +#[frame_support::pallet] +pub mod pallet2 { + use super::*; + use frame_support::pallet_prelude::*; + + #[pallet::error] + pub enum Error {} + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::pallet] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::storage] + pub type SomeValue, I: 'static = ()> = StorageValue<_, u32>; + + #[pallet::storage] + pub type SomeMap, I: 'static = ()> = + StorageMap<_, Twox64Concat, u32, u32, OptionQuery>; + + #[pallet::view_functions_experimental] + impl, I: 'static> Pallet + where + T::AccountId: From + SomeAssociation1, + { + /// Query value no args. + pub fn get_value() -> Option { + SomeValue::::get() + } + + /// Query value with args. + pub fn get_value_with_arg(key: u32) -> Option { + SomeMap::::get(key) + } + } +} diff --git a/substrate/frame/examples/view-functions/src/tests.rs b/substrate/frame/examples/view-functions/src/tests.rs new file mode 100644 index 000000000000..6e0101ed2094 --- /dev/null +++ b/substrate/frame/examples/view-functions/src/tests.rs @@ -0,0 +1,189 @@ +// 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. + +//! Tests for `pallet-example-view-functions`. +#![cfg(test)] + +use crate::{ + pallet::{self, Pallet}, + pallet2, +}; +use codec::{Decode, Encode}; +use scale_info::{form::PortableForm, meta_type}; + +use frame_metadata::RuntimeMetadata; +use frame_support::{derive_impl, pallet_prelude::PalletInfoAccess, traits::ViewFunction}; +use sp_io::hashing::twox_128; +use sp_metadata_ir::{ViewFunctionArgMetadataIR, ViewFunctionGroupIR, ViewFunctionMetadataIR}; +use sp_runtime::testing::TestXt; + +pub type AccountId = u32; +pub type Balance = u32; + +type Block = frame_system::mocking::MockBlock; +frame_support::construct_runtime!( + pub enum Runtime { + System: frame_system, + ViewFunctionsExample: pallet, + ViewFunctionsInstance: pallet2, + ViewFunctionsInstance1: pallet2::, + } +); + +pub type Extrinsic = TestXt; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Runtime { + type Block = Block; +} + +impl pallet::Config for Runtime {} +impl pallet2::Config for Runtime {} + +impl pallet2::Config for Runtime {} + +pub fn new_test_ext() -> sp_io::TestExternalities { + use sp_runtime::BuildStorage; + + let t = RuntimeGenesisConfig { system: Default::default() }.build_storage().unwrap(); + t.into() +} + +#[test] +fn pallet_get_value_query() { + new_test_ext().execute_with(|| { + let some_value = Some(99); + pallet::SomeValue::::set(some_value); + assert_eq!(some_value, Pallet::::get_value()); + + let query = pallet::GetValueViewFunction::::new(); + test_dispatch_view_function(&query, some_value); + }); +} + +#[test] +fn pallet_get_value_with_arg_query() { + new_test_ext().execute_with(|| { + let some_key = 1u32; + let some_value = Some(123); + pallet::SomeMap::::set(some_key, some_value); + assert_eq!(some_value, Pallet::::get_value_with_arg(some_key)); + + let query = pallet::GetValueWithArgViewFunction::::new(some_key); + test_dispatch_view_function(&query, some_value); + }); +} + +#[test] +fn pallet_multiple_instances() { + use pallet2::Instance1; + + new_test_ext().execute_with(|| { + let instance_value = Some(123); + let instance1_value = Some(456); + + pallet2::SomeValue::::set(instance_value); + pallet2::SomeValue::::set(instance1_value); + + let query = pallet2::GetValueViewFunction::::new(); + test_dispatch_view_function(&query, instance_value); + + let query_instance1 = pallet2::GetValueViewFunction::::new(); + test_dispatch_view_function(&query_instance1, instance1_value); + }); +} + +#[test] +fn metadata_ir_definitions() { + new_test_ext().execute_with(|| { + let metadata_ir = Runtime::metadata_ir(); + let pallet1 = metadata_ir + .view_functions + .groups + .iter() + .find(|pallet| pallet.name == "ViewFunctionsExample") + .unwrap(); + + fn view_fn_id(preifx_hash: [u8; 16], view_fn_signature: &str) -> [u8; 32] { + let mut id = [0u8; 32]; + id[..16].copy_from_slice(&preifx_hash); + id[16..].copy_from_slice(&twox_128(view_fn_signature.as_bytes())); + id + } + + let get_value_id = view_fn_id( + ::name_hash(), + "get_value() -> Option", + ); + + let get_value_with_arg_id = view_fn_id( + ::name_hash(), + "get_value_with_arg(u32) -> Option", + ); + + pretty_assertions::assert_eq!( + pallet1.view_functions, + vec![ + ViewFunctionMetadataIR { + name: "get_value", + id: get_value_id, + args: vec![], + output: meta_type::>(), + docs: vec![" Query value no args."], + }, + ViewFunctionMetadataIR { + name: "get_value_with_arg", + id: get_value_with_arg_id, + args: vec![ViewFunctionArgMetadataIR { name: "key", ty: meta_type::() },], + output: meta_type::>(), + docs: vec![" Query value with args."], + }, + ] + ); + }); +} + +#[test] +fn metadata_encoded_to_custom_value() { + new_test_ext().execute_with(|| { + let metadata = sp_metadata_ir::into_latest(Runtime::metadata_ir()); + // metadata is currently experimental so lives as a custom value. + let frame_metadata::RuntimeMetadata::V15(v15) = metadata.1 else { + panic!("Expected metadata v15") + }; + let custom_value = v15 + .custom + .map + .get("view_functions_experimental") + .expect("Expected custom value"); + let view_function_groups: Vec> = + Decode::decode(&mut &custom_value.value[..]).unwrap(); + assert_eq!(view_function_groups.len(), 4); + }); +} + +fn test_dispatch_view_function(query: &Q, expected: V) +where + Q: ViewFunction + Encode, + V: Decode + Eq + PartialEq + std::fmt::Debug, +{ + let input = query.encode(); + let output = Runtime::execute_view_function(Q::id(), input).unwrap(); + let query_result = V::decode(&mut &output[..]).unwrap(); + + assert_eq!(expected, query_result,); +} diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs index 4590a3a7f490..3e2fa14bee99 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/metadata.rs @@ -78,6 +78,18 @@ pub fn expand_runtime_metadata( }) .collect::>(); + let view_functions = pallet_declarations.iter().map(|decl| { + let name = &decl.name; + let path = &decl.path; + let instance = decl.instance.as_ref().into_iter(); + + quote! { + #path::Pallet::<#runtime #(, #path::#instance)*>::pallet_view_functions_metadata( + ::core::stringify!(#name) + ) + } + }); + quote! { impl #runtime { fn metadata_ir() -> #scrate::__private::metadata_ir::MetadataIR { @@ -147,6 +159,12 @@ pub fn expand_runtime_metadata( >(), event_enum_ty: #scrate::__private::scale_info::meta_type::(), error_enum_ty: #scrate::__private::scale_info::meta_type::(), + }, + view_functions: #scrate::__private::metadata_ir::RuntimeViewFunctionsIR { + ty: #scrate::__private::scale_info::meta_type::< + <#runtime as #system_path::Config>::RuntimeViewFunction + >(), + groups: #scrate::__private::sp_std::vec![ #(#view_functions),* ], } } } diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs index 88f9a3c6e33f..823aa69dbdf2 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs @@ -28,6 +28,7 @@ mod outer_enums; mod slash_reason; mod task; mod unsigned; +mod view_function; pub use call::expand_outer_dispatch; pub use config::expand_outer_config; @@ -41,3 +42,4 @@ 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; +pub use view_function::expand_outer_query; diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/view_function.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/view_function.rs new file mode 100644 index 000000000000..9e0b13aa9125 --- /dev/null +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/view_function.rs @@ -0,0 +1,76 @@ +// 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::Pallet; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; + +/// Expands implementation of runtime level `DispatchViewFunction`. +pub fn expand_outer_query( + runtime_name: &Ident, + pallet_decls: &[Pallet], + scrate: &TokenStream2, +) -> TokenStream2 { + let runtime_view_function = syn::Ident::new("RuntimeViewFunction", Span::call_site()); + + let prefix_conditionals = pallet_decls.iter().map(|pallet| { + let pallet_name = &pallet.name; + quote::quote! { + if id.prefix == <#pallet_name as #scrate::traits::ViewFunctionIdPrefix>::prefix() { + return <#pallet_name as #scrate::traits::DispatchViewFunction>::dispatch_view_function(id, input, output) + } + } + }); + + quote::quote! { + /// Runtime query type. + #[derive( + Clone, PartialEq, Eq, + #scrate::__private::codec::Encode, + #scrate::__private::codec::Decode, + #scrate::__private::scale_info::TypeInfo, + #scrate::__private::RuntimeDebug, + )] + pub enum #runtime_view_function {} + + const _: () = { + impl #scrate::traits::DispatchViewFunction for #runtime_view_function { + fn dispatch_view_function( + id: & #scrate::__private::ViewFunctionId, + input: &mut &[u8], + output: &mut O + ) -> Result<(), #scrate::__private::ViewFunctionDispatchError> + { + #( #prefix_conditionals )* + Err(#scrate::__private::ViewFunctionDispatchError::NotFound(id.clone())) + } + } + + impl #runtime_name { + /// Convenience function for query execution from the runtime API. + pub fn execute_view_function( + id: #scrate::__private::ViewFunctionId, + input: #scrate::__private::sp_std::vec::Vec<::core::primitive::u8>, + ) -> Result<#scrate::__private::sp_std::vec::Vec<::core::primitive::u8>, #scrate::__private::ViewFunctionDispatchError> + { + let mut output = #scrate::__private::sp_std::vec![]; + <#runtime_view_function as #scrate::traits::DispatchViewFunction>::dispatch_view_function(&id, &mut &input[..], &mut output)?; + Ok(output) + } + } + }; + } +} diff --git a/substrate/frame/support/procedural/src/construct_runtime/mod.rs b/substrate/frame/support/procedural/src/construct_runtime/mod.rs index 17042c248780..c41f46ac8921 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/mod.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/mod.rs @@ -400,6 +400,7 @@ fn construct_runtime_final_expansion( let dispatch = expand::expand_outer_dispatch(&name, system_pallet, &pallets, &scrate); let tasks = expand::expand_outer_task(&name, &pallets, &scrate); + let query = expand::expand_outer_query(&name, &pallets, &scrate); let metadata = expand::expand_runtime_metadata( &name, &pallets, @@ -491,6 +492,8 @@ fn construct_runtime_final_expansion( #tasks + #query + #metadata #outer_config diff --git a/substrate/frame/support/procedural/src/construct_runtime/parse.rs b/substrate/frame/support/procedural/src/construct_runtime/parse.rs index 729a803a302e..fb18cbf0de79 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/parse.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/parse.rs @@ -45,6 +45,7 @@ mod keyword { syn::custom_keyword!(Task); syn::custom_keyword!(LockId); syn::custom_keyword!(SlashReason); + syn::custom_keyword!(Query); syn::custom_keyword!(exclude_parts); syn::custom_keyword!(use_parts); syn::custom_keyword!(expanded); @@ -393,6 +394,7 @@ pub enum PalletPartKeyword { Task(keyword::Task), LockId(keyword::LockId), SlashReason(keyword::SlashReason), + Query(keyword::Query), } impl Parse for PalletPartKeyword { @@ -427,6 +429,8 @@ impl Parse for PalletPartKeyword { Ok(Self::LockId(input.parse()?)) } else if lookahead.peek(keyword::SlashReason) { Ok(Self::SlashReason(input.parse()?)) + } else if lookahead.peek(keyword::Query) { + Ok(Self::Query(input.parse()?)) } else { Err(lookahead.error()) } @@ -451,6 +455,7 @@ impl PalletPartKeyword { Self::Task(_) => "Task", Self::LockId(_) => "LockId", Self::SlashReason(_) => "SlashReason", + Self::Query(_) => "Query", } } @@ -482,6 +487,7 @@ impl ToTokens for PalletPartKeyword { Self::Task(inner) => inner.to_tokens(tokens), Self::LockId(inner) => inner.to_tokens(tokens), Self::SlashReason(inner) => inner.to_tokens(tokens), + Self::Query(inner) => inner.to_tokens(tokens), } } } diff --git a/substrate/frame/support/procedural/src/lib.rs b/substrate/frame/support/procedural/src/lib.rs index c2f546d92048..26703a2438ef 100644 --- a/substrate/frame/support/procedural/src/lib.rs +++ b/substrate/frame/support/procedural/src/lib.rs @@ -817,6 +817,7 @@ pub fn inject_runtime_type(_: TokenStream, tokens: TokenStream) -> TokenStream { if item.ident != "RuntimeCall" && item.ident != "RuntimeEvent" && item.ident != "RuntimeTask" && + item.ident != "RuntimeViewFunction" && item.ident != "RuntimeOrigin" && item.ident != "RuntimeHoldReason" && item.ident != "RuntimeFreezeReason" && @@ -826,7 +827,7 @@ pub fn inject_runtime_type(_: TokenStream, tokens: TokenStream) -> TokenStream { return syn::Error::new_spanned( item, "`#[inject_runtime_type]` can only be attached to `RuntimeCall`, `RuntimeEvent`, \ - `RuntimeTask`, `RuntimeOrigin`, `RuntimeParameters` or `PalletInfo`", + `RuntimeTask`, `RuntimeViewFunction`, `RuntimeOrigin`, `RuntimeParameters` or `PalletInfo`", ) .to_compile_error() .into(); diff --git a/substrate/frame/support/procedural/src/pallet/expand/mod.rs b/substrate/frame/support/procedural/src/pallet/expand/mod.rs index 3f9b50f79c0c..439ec55e269d 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/mod.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/mod.rs @@ -35,6 +35,7 @@ mod tasks; mod tt_default_parts; mod type_value; mod validate_unsigned; +mod view_functions; mod warnings; use crate::pallet::Def; @@ -66,6 +67,7 @@ pub fn expand(mut def: Def) -> proc_macro2::TokenStream { let error = error::expand_error(&mut def); let event = event::expand_event(&mut def); let storages = storage::expand_storages(&mut def); + let view_functions = view_functions::expand_view_functions(&def); let inherents = inherent::expand_inherents(&mut def); let instances = instances::expand_instances(&mut def); let hooks = hooks::expand_hooks(&mut def); @@ -108,6 +110,7 @@ storage item. Otherwise, all storage items are listed among [*Type Definitions*] #error #event #storages + #view_functions #inherents #instances #hooks diff --git a/substrate/frame/support/procedural/src/pallet/expand/view_functions.rs b/substrate/frame/support/procedural/src/pallet/expand/view_functions.rs new file mode 100644 index 000000000000..3aa0b1c93e89 --- /dev/null +++ b/substrate/frame/support/procedural/src/pallet/expand/view_functions.rs @@ -0,0 +1,257 @@ +// 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::pallet::{parse::view_functions::ViewFunctionDef, Def}; +use proc_macro2::{Span, TokenStream}; +use syn::spanned::Spanned; + +pub fn expand_view_functions(def: &Def) -> TokenStream { + let (span, where_clause, view_fns, docs) = match def.view_functions.as_ref() { + Some(view_fns) => ( + view_fns.attr_span.clone(), + view_fns.where_clause.clone(), + view_fns.view_functions.clone(), + view_fns.docs.clone(), + ), + None => (def.item.span(), def.config.where_clause.clone(), Vec::new(), Vec::new()), + }; + + let view_function_prefix_impl = + expand_view_function_prefix_impl(def, span, where_clause.as_ref()); + + let view_fn_impls = view_fns + .iter() + .map(|view_fn| expand_view_function(def, span, where_clause.as_ref(), view_fn)); + let impl_dispatch_view_function = + impl_dispatch_view_function(def, span, where_clause.as_ref(), &view_fns); + let impl_view_function_metadata = + impl_view_function_metadata(def, span, where_clause.as_ref(), &view_fns, &docs); + + quote::quote! { + #view_function_prefix_impl + #( #view_fn_impls )* + #impl_dispatch_view_function + #impl_view_function_metadata + } +} + +fn expand_view_function_prefix_impl( + def: &Def, + span: Span, + where_clause: Option<&syn::WhereClause>, +) -> TokenStream { + let pallet_ident = &def.pallet_struct.pallet; + let frame_support = &def.frame_support; + let frame_system = &def.frame_system; + let type_impl_gen = &def.type_impl_generics(span); + let type_use_gen = &def.type_use_generics(span); + + quote::quote! { + impl<#type_impl_gen> #frame_support::traits::ViewFunctionIdPrefix for #pallet_ident<#type_use_gen> #where_clause { + fn prefix() -> [::core::primitive::u8; 16usize] { + < + ::PalletInfo + as #frame_support::traits::PalletInfo + >::name_hash::>() + .expect("No name_hash found for the pallet in the runtime! This usually means that the pallet wasn't added to `construct_runtime!`.") + } + } + } +} + +fn expand_view_function( + def: &Def, + span: Span, + where_clause: Option<&syn::WhereClause>, + view_fn: &ViewFunctionDef, +) -> TokenStream { + let frame_support = &def.frame_support; + let pallet_ident = &def.pallet_struct.pallet; + let type_impl_gen = &def.type_impl_generics(span); + let type_decl_bounded_gen = &def.type_decl_bounded_generics(span); + let type_use_gen = &def.type_use_generics(span); + let capture_docs = if cfg!(feature = "no-metadata-docs") { "never" } else { "always" }; + + let view_function_struct_ident = view_fn.view_function_struct_ident(); + let view_fn_name = &view_fn.name; + let (arg_names, arg_types) = view_fn.args_names_types(); + let return_type = &view_fn.return_type; + let docs = &view_fn.docs; + + let view_function_id_suffix_bytes = view_fn + .view_function_id_suffix_bytes() + .map(|byte| syn::LitInt::new(&format!("0x{:X}_u8", byte), Span::call_site())); + + quote::quote! { + #( #[doc = #docs] )* + #[allow(missing_docs)] + #[derive( + #frame_support::RuntimeDebugNoBound, + #frame_support::CloneNoBound, + #frame_support::EqNoBound, + #frame_support::PartialEqNoBound, + #frame_support::__private::codec::Encode, + #frame_support::__private::codec::Decode, + #frame_support::__private::scale_info::TypeInfo, + )] + #[codec(encode_bound())] + #[codec(decode_bound())] + #[scale_info(skip_type_params(#type_use_gen), capture_docs = #capture_docs)] + pub struct #view_function_struct_ident<#type_decl_bounded_gen> #where_clause { + #( + pub #arg_names: #arg_types, + )* + _marker: ::core::marker::PhantomData<(#type_use_gen,)>, + } + + impl<#type_impl_gen> #view_function_struct_ident<#type_use_gen> #where_clause { + /// Create a new [`#view_function_struct_ident`] instance. + pub fn new(#( #arg_names: #arg_types, )*) -> Self { + Self { + #( #arg_names, )* + _marker: ::core::default::Default::default() + } + } + } + + impl<#type_impl_gen> #frame_support::traits::ViewFunctionIdSuffix for #view_function_struct_ident<#type_use_gen> #where_clause { + const SUFFIX: [::core::primitive::u8; 16usize] = [ #( #view_function_id_suffix_bytes ),* ]; + } + + impl<#type_impl_gen> #frame_support::traits::ViewFunction for #view_function_struct_ident<#type_use_gen> #where_clause { + fn id() -> #frame_support::__private::ViewFunctionId { + #frame_support::__private::ViewFunctionId { + prefix: <#pallet_ident<#type_use_gen> as #frame_support::traits::ViewFunctionIdPrefix>::prefix(), + suffix: ::SUFFIX, + } + } + + type ReturnType = #return_type; + + fn invoke(self) -> Self::ReturnType { + let Self { #( #arg_names, )* _marker } = self; + #pallet_ident::<#type_use_gen> :: #view_fn_name( #( #arg_names, )* ) + } + } + } +} + +fn impl_dispatch_view_function( + def: &Def, + span: Span, + where_clause: Option<&syn::WhereClause>, + view_fns: &[ViewFunctionDef], +) -> TokenStream { + let frame_support = &def.frame_support; + let pallet_ident = &def.pallet_struct.pallet; + let type_impl_gen = &def.type_impl_generics(span); + let type_use_gen = &def.type_use_generics(span); + + let query_match_arms = view_fns.iter().map(|view_fn| { + let view_function_struct_ident = view_fn.view_function_struct_ident(); + quote::quote! { + <#view_function_struct_ident<#type_use_gen> as #frame_support::traits::ViewFunctionIdSuffix>::SUFFIX => { + <#view_function_struct_ident<#type_use_gen> as #frame_support::traits::ViewFunction>::execute(input, output) + } + } + }); + + quote::quote! { + impl<#type_impl_gen> #frame_support::traits::DispatchViewFunction + for #pallet_ident<#type_use_gen> #where_clause + { + #[deny(unreachable_patterns)] + fn dispatch_view_function( + id: & #frame_support::__private::ViewFunctionId, + input: &mut &[u8], + output: &mut O + ) -> Result<(), #frame_support::__private::ViewFunctionDispatchError> + { + match id.suffix { + #( #query_match_arms )* + _ => Err(#frame_support::__private::ViewFunctionDispatchError::NotFound(id.clone())), + } + } + } + } +} + +fn impl_view_function_metadata( + def: &Def, + span: Span, + where_clause: Option<&syn::WhereClause>, + view_fns: &[ViewFunctionDef], + docs: &[syn::Expr], +) -> TokenStream { + let frame_support = &def.frame_support; + let pallet_ident = &def.pallet_struct.pallet; + let type_impl_gen = &def.type_impl_generics(span); + let type_use_gen = &def.type_use_generics(span); + + let view_functions = view_fns.iter().map(|view_fn| { + let view_function_struct_ident = view_fn.view_function_struct_ident(); + let name = &view_fn.name; + let args = view_fn.args.iter().filter_map(|fn_arg| { + match fn_arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(typed) => { + let pat = &typed.pat; + let ty = &typed.ty; + Some(quote::quote! { + #frame_support::__private::metadata_ir::ViewFunctionArgMetadataIR { + name: ::core::stringify!(#pat), + ty: #frame_support::__private::scale_info::meta_type::<#ty>(), + } + }) + } + } + }); + + let no_docs = vec![]; + let doc = if cfg!(feature = "no-metadata-docs") { &no_docs } else { &view_fn.docs }; + + quote::quote! { + #frame_support::__private::metadata_ir::ViewFunctionMetadataIR { + name: ::core::stringify!(#name), + id: <#view_function_struct_ident<#type_use_gen> as #frame_support::traits::ViewFunction>::id().into(), + args: #frame_support::__private::sp_std::vec![ #( #args ),* ], + output: #frame_support::__private::scale_info::meta_type::< + <#view_function_struct_ident<#type_use_gen> as #frame_support::traits::ViewFunction>::ReturnType + >(), + docs: #frame_support::__private::sp_std::vec![ #( #doc ),* ], + } + } + }); + + let no_docs = vec![]; + let doc = if cfg!(feature = "no-metadata-docs") { &no_docs } else { docs }; + + quote::quote! { + impl<#type_impl_gen> #pallet_ident<#type_use_gen> #where_clause { + #[doc(hidden)] + pub fn pallet_view_functions_metadata(name: &'static ::core::primitive::str) + -> #frame_support::__private::metadata_ir::ViewFunctionGroupIR + { + #frame_support::__private::metadata_ir::ViewFunctionGroupIR { + name, + view_functions: #frame_support::__private::sp_std::vec![ #( #view_functions ),* ], + docs: #frame_support::__private::sp_std::vec![ #( #doc ),* ], + } + } + } + } +} diff --git a/substrate/frame/support/procedural/src/pallet/parse/mod.rs b/substrate/frame/support/procedural/src/pallet/parse/mod.rs index c9a150effccb..89875974b8b5 100644 --- a/substrate/frame/support/procedural/src/pallet/parse/mod.rs +++ b/substrate/frame/support/procedural/src/pallet/parse/mod.rs @@ -36,6 +36,7 @@ pub mod storage; pub mod tasks; pub mod type_value; pub mod validate_unsigned; +pub mod view_functions; #[cfg(test)] pub mod tests; @@ -70,6 +71,7 @@ pub struct Def { pub frame_system: syn::Path, pub frame_support: syn::Path, pub dev_mode: bool, + pub view_functions: Option, } impl Def { @@ -103,6 +105,7 @@ impl Def { let mut storages = vec![]; let mut type_values = vec![]; let mut composites: Vec = vec![]; + let mut view_functions = None; for (index, item) in items.iter_mut().enumerate() { let pallet_attr: Option = helper::take_first_item_pallet_attr(item)?; @@ -205,6 +208,9 @@ impl Def { } composites.push(composite); }, + Some(PalletAttr::ViewFunctions(span)) => { + view_functions = Some(view_functions::ViewFunctionsImplDef::try_from(span, item)?); + } Some(attr) => { let msg = "Invalid duplicated attribute"; return Err(syn::Error::new(attr.span(), msg)) @@ -250,6 +256,7 @@ impl Def { frame_system, frame_support, dev_mode, + view_functions, }; def.check_instance_usage()?; @@ -563,6 +570,7 @@ mod keyword { syn::custom_keyword!(pallet); syn::custom_keyword!(extra_constants); syn::custom_keyword!(composite_enum); + syn::custom_keyword!(view_functions_experimental); } /// The possible values for the `#[pallet::config]` attribute. @@ -652,6 +660,7 @@ enum PalletAttr { TypeValue(proc_macro2::Span), ExtraConstants(proc_macro2::Span), Composite(proc_macro2::Span), + ViewFunctions(proc_macro2::Span), } impl PalletAttr { @@ -677,6 +686,7 @@ impl PalletAttr { Self::TypeValue(span) => *span, Self::ExtraConstants(span) => *span, Self::Composite(span) => *span, + Self::ViewFunctions(span) => *span, } } } @@ -778,6 +788,10 @@ impl syn::parse::Parse for PalletAttr { Ok(PalletAttr::ExtraConstants(content.parse::()?.span())) } else if lookahead.peek(keyword::composite_enum) { Ok(PalletAttr::Composite(content.parse::()?.span())) + } else if lookahead.peek(keyword::view_functions_experimental) { + Ok(PalletAttr::ViewFunctions( + content.parse::()?.span(), + )) } else { Err(lookahead.error()) } diff --git a/substrate/frame/support/procedural/src/pallet/parse/view_functions.rs b/substrate/frame/support/procedural/src/pallet/parse/view_functions.rs new file mode 100644 index 000000000000..51df02c7a6a8 --- /dev/null +++ b/substrate/frame/support/procedural/src/pallet/parse/view_functions.rs @@ -0,0 +1,144 @@ +// 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 governsing permissions and +// limitations under the License. + +use frame_support_procedural_tools::get_doc_literals; +use inflector::Inflector; +use syn::spanned::Spanned; + +/// Parsed representation of an impl block annotated with `pallet::view_functions_experimental`. +pub struct ViewFunctionsImplDef { + /// The where_clause used. + pub where_clause: Option, + /// The span of the pallet::view_functions_experimental attribute. + pub attr_span: proc_macro2::Span, + /// Docs, specified on the impl Block. + pub docs: Vec, + /// The view function definitions. + pub view_functions: Vec, +} + +impl ViewFunctionsImplDef { + pub fn try_from(attr_span: proc_macro2::Span, item: &mut syn::Item) -> syn::Result { + let item_impl = if let syn::Item::Impl(item) = item { + item + } else { + return Err(syn::Error::new( + item.span(), + "Invalid pallet::view_functions_experimental, expected item impl", + )) + }; + let mut view_functions = Vec::new(); + for item in &mut item_impl.items { + if let syn::ImplItem::Fn(method) = item { + if !matches!(method.vis, syn::Visibility::Public(_)) { + let msg = "Invalid pallet::view_functions_experimental, view function must be public: \ + `pub fn`"; + + let span = match method.vis { + syn::Visibility::Inherited => method.sig.span(), + _ => method.vis.span(), + }; + + return Err(syn::Error::new(span, msg)) + } + + let view_fn_def = ViewFunctionDef::try_from(method.clone())?; + view_functions.push(view_fn_def) + } + } + Ok(Self { + view_functions, + attr_span, + where_clause: item_impl.generics.where_clause.clone(), + docs: get_doc_literals(&item_impl.attrs), + }) + } +} + +/// Parsed representation of a view function definition. +#[derive(Clone)] +pub struct ViewFunctionDef { + pub name: syn::Ident, + pub docs: Vec, + pub args: Vec, + pub return_type: syn::Type, +} + +impl TryFrom for ViewFunctionDef { + type Error = syn::Error; + fn try_from(method: syn::ImplItemFn) -> Result { + let syn::ReturnType::Type(_, type_) = method.sig.output else { + return Err(syn::Error::new( + method.sig.output.span(), + "view functions must return a value", + )) + }; + + Ok(Self { + name: method.sig.ident.clone(), + docs: get_doc_literals(&method.attrs), + args: method.sig.inputs.iter().cloned().collect::>(), + return_type: *type_.clone(), + }) + } +} + +impl ViewFunctionDef { + pub fn view_function_struct_ident(&self) -> syn::Ident { + syn::Ident::new( + &format!("{}ViewFunction", self.name.to_string().to_pascal_case()), + self.name.span(), + ) + } + + pub fn view_function_id_suffix_bytes(&self) -> [u8; 16] { + let mut output = [0u8; 16]; + + // concatenate the signature string + let arg_types = self + .args_names_types() + .1 + .iter() + .map(|ty| quote::quote!(#ty).to_string().replace(" ", "")) + .collect::>() + .join(","); + let return_type = &self.return_type; + let return_type = quote::quote!(#return_type).to_string().replace(" ", ""); + let view_fn_signature = format!( + "{view_function_name}({arg_types}) -> {return_type}", + view_function_name = &self.name, + ); + + // hash the signature string + let hash = sp_crypto_hashing::twox_128(view_fn_signature.as_bytes()); + output.copy_from_slice(&hash[..]); + output + } + + pub fn args_names_types(&self) -> (Vec, Vec) { + self.args + .iter() + .map(|arg| match arg { + syn::FnArg::Typed(pat_type) => match &*pat_type.pat { + syn::Pat::Ident(ident) => (ident.ident.clone(), *pat_type.ty.clone()), + _ => panic!("Unsupported pattern in view function argument"), + }, + _ => panic!("Unsupported argument in view function"), + }) + .unzip() + } +} diff --git a/substrate/frame/support/procedural/src/runtime/expand/mod.rs b/substrate/frame/support/procedural/src/runtime/expand/mod.rs index 666bc03aa415..005b109c0eb5 100644 --- a/substrate/frame/support/procedural/src/runtime/expand/mod.rs +++ b/substrate/frame/support/procedural/src/runtime/expand/mod.rs @@ -182,6 +182,7 @@ fn construct_runtime_final_expansion( let mut slash_reason = None; let mut lock_id = None; let mut task = None; + let mut query = None; for runtime_type in runtime_types.iter() { match runtime_type { @@ -224,6 +225,9 @@ fn construct_runtime_final_expansion( RuntimeType::RuntimeTask(_) => { task = Some(expand::expand_outer_task(&name, &pallets, &scrate)); }, + RuntimeType::RuntimeViewFunction(_) => { + query = Some(expand::expand_outer_query(&name, &pallets, &scrate)); + }, } } @@ -301,6 +305,8 @@ fn construct_runtime_final_expansion( #task + #query + #metadata #outer_config diff --git a/substrate/frame/support/procedural/src/runtime/parse/runtime_types.rs b/substrate/frame/support/procedural/src/runtime/parse/runtime_types.rs index a4480e2a1fd3..9a385146a811 100644 --- a/substrate/frame/support/procedural/src/runtime/parse/runtime_types.rs +++ b/substrate/frame/support/procedural/src/runtime/parse/runtime_types.rs @@ -32,6 +32,7 @@ mod keyword { custom_keyword!(RuntimeSlashReason); custom_keyword!(RuntimeLockId); custom_keyword!(RuntimeTask); + custom_keyword!(RuntimeViewFunction); } #[derive(Debug, Clone, PartialEq)] @@ -45,6 +46,7 @@ pub enum RuntimeType { RuntimeSlashReason(keyword::RuntimeSlashReason), RuntimeLockId(keyword::RuntimeLockId), RuntimeTask(keyword::RuntimeTask), + RuntimeViewFunction(keyword::RuntimeViewFunction), } impl Parse for RuntimeType { @@ -69,6 +71,8 @@ impl Parse for RuntimeType { Ok(Self::RuntimeLockId(input.parse()?)) } else if lookahead.peek(keyword::RuntimeTask) { Ok(Self::RuntimeTask(input.parse()?)) + } else if lookahead.peek(keyword::RuntimeViewFunction) { + Ok(Self::RuntimeViewFunction(input.parse()?)) } else { Err(lookahead.error()) } diff --git a/substrate/frame/support/src/dispatch.rs b/substrate/frame/support/src/dispatch.rs index 483a3dce77f6..5a2b79b87113 100644 --- a/substrate/frame/support/src/dispatch.rs +++ b/substrate/frame/support/src/dispatch.rs @@ -719,6 +719,7 @@ mod weight_tests { type RuntimeOrigin; type RuntimeCall; type RuntimeTask; + type RuntimeViewFunction; type PalletInfo: crate::traits::PalletInfo; type DbWeight: Get; } @@ -829,6 +830,7 @@ mod weight_tests { type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; type RuntimeTask = RuntimeTask; + type RuntimeViewFunction = RuntimeViewFunction; type DbWeight = DbWeight; type PalletInfo = PalletInfo; } diff --git a/substrate/frame/support/src/lib.rs b/substrate/frame/support/src/lib.rs index c64987b17d35..61ad36f8bf57 100644 --- a/substrate/frame/support/src/lib.rs +++ b/substrate/frame/support/src/lib.rs @@ -54,7 +54,7 @@ pub mod __private { pub use scale_info; pub use serde; pub use serde_json; - pub use sp_core::{Get, OpaqueMetadata, Void}; + pub use sp_core::{Get, OpaqueMetadata, ViewFunctionDispatchError, ViewFunctionId, Void}; pub use sp_crypto_hashing_proc_macro; pub use sp_inherents; #[cfg(feature = "std")] diff --git a/substrate/frame/support/src/storage/generator/mod.rs b/substrate/frame/support/src/storage/generator/mod.rs index b0b1bda24bb7..bc6e2b2278eb 100644 --- a/substrate/frame/support/src/storage/generator/mod.rs +++ b/substrate/frame/support/src/storage/generator/mod.rs @@ -65,6 +65,7 @@ mod tests { type RuntimeOrigin; type RuntimeCall; type RuntimeTask; + type RuntimeViewFunction; type PalletInfo: crate::traits::PalletInfo; type DbWeight: Get; } @@ -132,6 +133,7 @@ mod tests { type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; type RuntimeTask = RuntimeTask; + type RuntimeViewFunction = RuntimeViewFunction; type PalletInfo = PalletInfo; type DbWeight = (); } diff --git a/substrate/frame/support/src/tests/mod.rs b/substrate/frame/support/src/tests/mod.rs index 7c90a12d4167..4ae3574352bd 100644 --- a/substrate/frame/support/src/tests/mod.rs +++ b/substrate/frame/support/src/tests/mod.rs @@ -51,6 +51,8 @@ pub mod frame_system { type PalletInfo = (); #[inject_runtime_type] type RuntimeTask = (); + #[inject_runtime_type] + type RuntimeViewFunction = (); type DbWeight = (); } } @@ -73,6 +75,8 @@ pub mod frame_system { #[pallet::no_default_bounds] type RuntimeTask: crate::traits::tasks::Task; #[pallet::no_default_bounds] + type RuntimeViewFunction: crate::traits::DispatchViewFunction; + #[pallet::no_default_bounds] type PalletInfo: crate::traits::PalletInfo; type DbWeight: Get; #[pallet::constant] diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs index 728426cc84c7..f59508341444 100644 --- a/substrate/frame/support/src/traits.rs +++ b/substrate/frame/support/src/traits.rs @@ -138,6 +138,12 @@ pub use proving::*; #[cfg(feature = "try-runtime")] mod try_runtime; + +mod view_function; +pub use view_function::{ + DispatchViewFunction, ViewFunction, ViewFunctionIdPrefix, ViewFunctionIdSuffix, +}; + #[cfg(feature = "try-runtime")] pub use try_runtime::{ Select as TryStateSelect, TryDecodeEntireStorage, TryDecodeEntireStorageError, TryState, diff --git a/substrate/frame/support/src/traits/view_function.rs b/substrate/frame/support/src/traits/view_function.rs new file mode 100644 index 000000000000..9a4de07cf465 --- /dev/null +++ b/substrate/frame/support/src/traits/view_function.rs @@ -0,0 +1,66 @@ +// 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 fsor the specific language governing permissions and +// limitations under the License. + +//! Traits for querying pallet view functions. + +use codec::{DecodeAll, Encode, Output}; +use sp_core::{ViewFunctionDispatchError, ViewFunctionId}; + +/// implemented by the runtime dispatching by prefix and then the pallet dispatching by suffix +pub trait DispatchViewFunction { + fn dispatch_view_function( + id: &ViewFunctionId, + input: &mut &[u8], + output: &mut O, + ) -> Result<(), ViewFunctionDispatchError>; +} + +impl DispatchViewFunction for () { + fn dispatch_view_function( + _id: &ViewFunctionId, + _input: &mut &[u8], + _output: &mut O, + ) -> Result<(), ViewFunctionDispatchError> { + Err(ViewFunctionDispatchError::NotImplemented) + } +} + +pub trait ViewFunctionIdPrefix { + fn prefix() -> [u8; 16]; +} + +pub trait ViewFunctionIdSuffix { + const SUFFIX: [u8; 16]; +} + +/// implemented for each pallet view function method +pub trait ViewFunction: DecodeAll { + fn id() -> ViewFunctionId; + type ReturnType: Encode; + + fn invoke(self) -> Self::ReturnType; + + fn execute( + input: &mut &[u8], + output: &mut O, + ) -> Result<(), ViewFunctionDispatchError> { + let view_function = Self::decode_all(input)?; + let result = view_function.invoke(); + Encode::encode_to(&result, output); + Ok(()) + } +} diff --git a/substrate/frame/support/test/tests/pallet.rs b/substrate/frame/support/test/tests/pallet.rs index 9df1f461bba2..68cda6009f9b 100644 --- a/substrate/frame/support/test/tests/pallet.rs +++ b/substrate/frame/support/test/tests/pallet.rs @@ -461,6 +461,22 @@ pub mod pallet { _myfield: u32, } + #[pallet::view_functions_experimental] + impl Pallet + where + T::AccountId: From + SomeAssociation1, + { + /// Query value no args. + pub fn get_value() -> Option { + Value::::get() + } + + /// Query value with args. + pub fn get_value_with_arg(key: u16) -> Option { + Map2::::get(key) + } + } + #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig where diff --git a/substrate/frame/support/test/tests/runtime_ui/pass/basic.rs b/substrate/frame/support/test/tests/runtime_ui/pass/basic.rs index 514f15018015..8350211335a5 100644 --- a/substrate/frame/support/test/tests/runtime_ui/pass/basic.rs +++ b/substrate/frame/support/test/tests/runtime_ui/pass/basic.rs @@ -27,7 +27,7 @@ impl frame_system::Config for Runtime { #[frame_support::runtime] mod runtime { #[runtime::runtime] - #[runtime::derive(RuntimeCall, RuntimeEvent, RuntimeOrigin, RuntimeError, RuntimeTask)] + #[runtime::derive(RuntimeCall, RuntimeEvent, RuntimeOrigin, RuntimeError, RuntimeTask, RuntimeViewFunction)] pub struct Runtime; #[runtime::pallet_index(0)] diff --git a/substrate/frame/system/src/lib.rs b/substrate/frame/system/src/lib.rs index 862fb4cf9faf..d17beceef14b 100644 --- a/substrate/frame/system/src/lib.rs +++ b/substrate/frame/system/src/lib.rs @@ -333,6 +333,8 @@ pub mod pallet { type PalletInfo = (); #[inject_runtime_type] type RuntimeTask = (); + #[inject_runtime_type] + type RuntimeViewFunction = (); type BaseCallFilter = frame_support::traits::Everything; type BlockHashCount = TestBlockHashCount>; type OnSetCode = (); @@ -425,6 +427,10 @@ pub mod pallet { #[inject_runtime_type] type RuntimeTask = (); + /// The query dispatch type, injected by `construct_runtime!`. + #[inject_runtime_type] + type RuntimeViewFunction = (); + /// Converts a module to the index of the module, injected by `construct_runtime!`. #[inject_runtime_type] type PalletInfo = (); @@ -515,6 +521,10 @@ pub mod pallet { #[pallet::no_default_bounds] type RuntimeTask: Task; + /// Type for dispatching queries. + #[pallet::no_default_bounds] + type RuntimeViewFunction: frame_support::traits::DispatchViewFunction; + /// This stores the number of previous transactions associated with a sender account. type Nonce: Parameter + Member diff --git a/substrate/primitives/api/Cargo.toml b/substrate/primitives/api/Cargo.toml index e0a4d06b2d81..3e6e10e63bec 100644 --- a/substrate/primitives/api/Cargo.toml +++ b/substrate/primitives/api/Cargo.toml @@ -24,6 +24,7 @@ sp-runtime-interface = { workspace = true } sp-externalities = { optional = true, workspace = true } sp-version = { workspace = true } sp-state-machine = { optional = true, workspace = true } +sp-std = { workspace = true } sp-trie = { optional = true, workspace = true } hash-db = { optional = true, workspace = true, default-features = true } thiserror = { optional = true, workspace = true } @@ -52,6 +53,7 @@ std = [ "sp-runtime-interface/std", "sp-runtime/std", "sp-state-machine/std", + "sp-std/std", "sp-test-primitives/std", "sp-trie/std", "sp-version/std", diff --git a/substrate/primitives/api/src/lib.rs b/substrate/primitives/api/src/lib.rs index b412d4b52fed..a366100f246b 100644 --- a/substrate/primitives/api/src/lib.rs +++ b/substrate/primitives/api/src/lib.rs @@ -115,7 +115,7 @@ pub mod __private { #[cfg(feature = "std")] pub use sp_core::traits::CallContext; -use sp_core::OpaqueMetadata; +use sp_core::{OpaqueMetadata, ViewFunctionDispatchError, ViewFunctionId}; #[cfg(feature = "std")] use sp_externalities::{Extension, Extensions}; #[cfg(feature = "std")] @@ -127,6 +127,7 @@ use sp_runtime::{traits::Block as BlockT, ExtrinsicInclusionMode}; pub use sp_state_machine::StorageProof; #[cfg(feature = "std")] use sp_state_machine::{backend::AsTrieBackend, Backend as StateBackend, OverlayedChanges}; +use sp_std::vec::Vec; use sp_version::RuntimeVersion; #[cfg(feature = "std")] use std::cell::RefCell; @@ -843,6 +844,12 @@ decl_runtime_apis! { /// This can be used to call `metadata_at_version`. fn metadata_versions() -> alloc::vec::Vec; } + + /// API for executing view function queriess + pub trait RuntimeViewFunction where { + /// Execute a view function query. + fn execute_view_function(query_id: ViewFunctionId, input: Vec) -> Result, ViewFunctionDispatchError>; + } } sp_core::generate_feature_enabled_macro!(std_enabled, feature = "std", $); diff --git a/substrate/primitives/core/src/lib.rs b/substrate/primitives/core/src/lib.rs index 454f61df7941..79f25552fc16 100644 --- a/substrate/primitives/core/src/lib.rs +++ b/substrate/primitives/core/src/lib.rs @@ -327,6 +327,44 @@ pub fn to_substrate_wasm_fn_return_value(value: &impl Encode) -> u64 { #[derive(Clone, Decode, Encode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum Void {} +/// Error type for view function dispatching. +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum ViewFunctionDispatchError { + /// View functions are not implemented for this runtime. + NotImplemented, + /// A view function with the given `ViewFunctionId` was not found + NotFound(ViewFunctionId), + /// Failed to decode the view function input. + Codec, +} + +impl From for ViewFunctionDispatchError { + fn from(_: codec::Error) -> Self { + ViewFunctionDispatchError::Codec + } +} + +/// The unique identifier for a view function. +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct ViewFunctionId { + /// The part of the id for dispatching view functions from the top level of the runtime. + /// + /// Specifies which view function grouping this view function belongs to. This could be a group + /// of view functions associated with a pallet, or a pallet agnostic group of view functions. + pub prefix: [u8; 16], + /// The part of the id for dispatching to a view function within a group. + pub suffix: [u8; 16], +} + +impl From for [u8; 32] { + fn from(value: ViewFunctionId) -> Self { + let mut output = [0u8; 32]; + output[..16].copy_from_slice(&value.prefix); + output[16..].copy_from_slice(&value.suffix); + output + } +} + /// Macro for creating `Maybe*` marker traits. /// /// Such a maybe-marker trait requires the given bound when `feature = std` and doesn't require diff --git a/substrate/primitives/metadata-ir/src/lib.rs b/substrate/primitives/metadata-ir/src/lib.rs index bf234432a1a6..9485de7e4dab 100644 --- a/substrate/primitives/metadata-ir/src/lib.rs +++ b/substrate/primitives/metadata-ir/src/lib.rs @@ -112,6 +112,7 @@ mod test { event_enum_ty: meta_type::<()>(), error_enum_ty: meta_type::<()>(), }, + view_functions: RuntimeViewFunctionsIR { ty: meta_type::<()>(), groups: vec![] }, } } diff --git a/substrate/primitives/metadata-ir/src/types.rs b/substrate/primitives/metadata-ir/src/types.rs index af217ffe16ee..0617fc7dfb94 100644 --- a/substrate/primitives/metadata-ir/src/types.rs +++ b/substrate/primitives/metadata-ir/src/types.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use codec::{Compact, Encode}; +use codec::{Compact, Decode, Encode}; use scale_info::{ form::{Form, MetaForm, PortableForm}, prelude::{collections::BTreeMap, vec::Vec}, @@ -41,6 +41,8 @@ pub struct MetadataIR { pub apis: Vec>, /// The outer enums types as found in the runtime. pub outer_enums: OuterEnumsIR, + /// Metadata of view function queries + pub view_functions: RuntimeViewFunctionsIR, } /// Metadata of a runtime trait. @@ -118,6 +120,89 @@ impl IntoPortable for RuntimeApiMethodParamMetadataIR { } } +/// Metadata of the top level runtime view function dispatch. +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug)] +pub struct RuntimeViewFunctionsIR { + /// The type implementing the runtime query dispatch. + pub ty: T::Type, + /// The view function groupings metadata. + pub groups: Vec>, +} + +/// Metadata of a runtime view function group. +/// +/// For example, view functions associated with a pallet would form a view function group. +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug)] +pub struct ViewFunctionGroupIR { + /// Name of the view function group. + pub name: T::String, + /// View functions belonging to the group. + pub view_functions: Vec>, + /// View function group documentation. + pub docs: Vec, +} + +impl IntoPortable for ViewFunctionGroupIR { + type Output = ViewFunctionGroupIR; + + fn into_portable(self, registry: &mut Registry) -> Self::Output { + ViewFunctionGroupIR { + name: self.name.into_portable(registry), + view_functions: registry.map_into_portable(self.view_functions), + docs: registry.map_into_portable(self.docs), + } + } +} + +/// Metadata of a runtime view function. +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug)] +pub struct ViewFunctionMetadataIR { + /// Query name. + pub name: T::String, + /// Query id. + pub id: [u8; 32], + /// Query args. + pub args: Vec>, + /// Query output. + pub output: T::Type, + /// Query documentation. + pub docs: Vec, +} + +impl IntoPortable for ViewFunctionMetadataIR { + type Output = ViewFunctionMetadataIR; + + fn into_portable(self, registry: &mut Registry) -> Self::Output { + ViewFunctionMetadataIR { + name: self.name.into_portable(registry), + id: self.id, + args: registry.map_into_portable(self.args), + output: registry.register_type(&self.output), + docs: registry.map_into_portable(self.docs), + } + } +} + +/// Metadata of a runtime method argument. +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug)] +pub struct ViewFunctionArgMetadataIR { + /// Query argument name. + pub name: T::String, + /// Query argument type. + pub ty: T::Type, +} + +impl IntoPortable for ViewFunctionArgMetadataIR { + type Output = ViewFunctionArgMetadataIR; + + fn into_portable(self, registry: &mut Registry) -> Self::Output { + ViewFunctionArgMetadataIR { + name: self.name.into_portable(registry), + ty: registry.register_type(&self.ty), + } + } +} + /// The intermediate representation for a pallet metadata. #[derive(Clone, PartialEq, Eq, Encode, Debug)] pub struct PalletMetadataIR { diff --git a/substrate/primitives/metadata-ir/src/v15.rs b/substrate/primitives/metadata-ir/src/v15.rs index ed315a31e6dc..7bc76f22b58d 100644 --- a/substrate/primitives/metadata-ir/src/v15.rs +++ b/substrate/primitives/metadata-ir/src/v15.rs @@ -17,31 +17,39 @@ //! Convert the IR to V15 metadata. -use crate::OuterEnumsIR; - use super::types::{ - ExtrinsicMetadataIR, MetadataIR, PalletMetadataIR, RuntimeApiMetadataIR, + ExtrinsicMetadataIR, MetadataIR, OuterEnumsIR, PalletMetadataIR, RuntimeApiMetadataIR, RuntimeApiMethodMetadataIR, RuntimeApiMethodParamMetadataIR, TransactionExtensionMetadataIR, }; use frame_metadata::v15::{ - CustomMetadata, ExtrinsicMetadata, OuterEnums, PalletMetadata, RuntimeApiMetadata, - RuntimeApiMethodMetadata, RuntimeApiMethodParamMetadata, RuntimeMetadataV15, - SignedExtensionMetadata, + CustomMetadata, CustomValueMetadata, ExtrinsicMetadata, OuterEnums, PalletMetadata, + RuntimeApiMetadata, RuntimeApiMethodMetadata, RuntimeApiMethodParamMetadata, + RuntimeMetadataV15, SignedExtensionMetadata, }; +use scale_info::{IntoPortable, Registry}; impl From for RuntimeMetadataV15 { fn from(ir: MetadataIR) -> Self { - RuntimeMetadataV15::new( - ir.pallets.into_iter().map(Into::into).collect(), - ir.extrinsic.into(), - ir.ty, - ir.apis.into_iter().map(Into::into).collect(), - ir.outer_enums.into(), - // Substrate does not collect yet the custom metadata fields. - // This allows us to extend the V15 easily. - CustomMetadata { map: Default::default() }, - ) + let mut registry = Registry::new(); + let pallets = + registry.map_into_portable(ir.pallets.into_iter().map(Into::::into)); + let extrinsic = Into::::into(ir.extrinsic).into_portable(&mut registry); + let ty = registry.register_type(&ir.ty); + let apis = + registry.map_into_portable(ir.apis.into_iter().map(Into::::into)); + let outer_enums = Into::::into(ir.outer_enums).into_portable(&mut registry); + + let view_function_groups = registry.map_into_portable(ir.view_functions.groups.into_iter()); + let view_functions_custom_metadata = CustomValueMetadata { + ty: ir.view_functions.ty, + value: codec::Encode::encode(&view_function_groups), + }; + let mut custom_map = alloc::collections::BTreeMap::new(); + custom_map.insert("view_functions_experimental", view_functions_custom_metadata); + let custom = CustomMetadata { map: custom_map }.into_portable(&mut registry); + + Self { types: registry.into(), pallets, extrinsic, ty, apis, outer_enums, custom } } } diff --git a/templates/minimal/runtime/src/lib.rs b/templates/minimal/runtime/src/lib.rs index 7b8449f2abe4..d13cd01bf85e 100644 --- a/templates/minimal/runtime/src/lib.rs +++ b/templates/minimal/runtime/src/lib.rs @@ -134,7 +134,8 @@ mod runtime { RuntimeHoldReason, RuntimeSlashReason, RuntimeLockId, - RuntimeTask + RuntimeTask, + RuntimeViewFunction )] pub struct Runtime; diff --git a/templates/solochain/runtime/src/lib.rs b/templates/solochain/runtime/src/lib.rs index ae0ea16ae42e..a70771ef6b1f 100644 --- a/templates/solochain/runtime/src/lib.rs +++ b/templates/solochain/runtime/src/lib.rs @@ -195,7 +195,8 @@ mod runtime { RuntimeHoldReason, RuntimeSlashReason, RuntimeLockId, - RuntimeTask + RuntimeTask, + RuntimeViewFunction )] pub struct Runtime;