diff --git a/Cargo.lock b/Cargo.lock
index 4b15c29b1b..4c12e28071 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1720,11 +1720,11 @@ dependencies = [
[[package]]
name = "frame-decode"
-version = "0.7.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50c554ce2394e2c04426a070b4cb133c72f6f14c86b665f4e13094addd8e8958"
+checksum = "a7cb8796f93fa038f979a014234d632e9688a120e745f936e2635123c77537f7"
dependencies = [
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"parity-scale-codec",
"scale-decode",
"scale-info",
@@ -1746,9 +1746,9 @@ dependencies = [
[[package]]
name = "frame-metadata"
-version = "20.0.0"
+version = "21.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26de808fa6461f2485dc51811aefed108850064994fb4a62b3ac21ffa62ac8df"
+checksum = "20dfd1d7eae1d94e32e869e2fb272d81f52dd8db57820a373adb83ea24d7d862"
dependencies = [
"cfg-if",
"parity-scale-codec",
@@ -1897,7 +1897,7 @@ dependencies = [
name = "generate-custom-metadata"
version = "0.41.0"
dependencies = [
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"parity-scale-codec",
"scale-info",
]
@@ -2407,12 +2407,13 @@ dependencies = [
"assert_matches",
"cfg_aliases",
"frame-decode",
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"futures",
"hex",
"parity-scale-codec",
"regex",
"scale-info",
+ "scale-value",
"serde",
"sp-core",
"substrate-runner",
@@ -5191,7 +5192,7 @@ dependencies = [
"bitvec",
"derive-where",
"either",
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"futures",
"hex",
"http-body",
@@ -5233,7 +5234,7 @@ version = "0.41.0"
dependencies = [
"clap",
"color-eyre",
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"heck",
"hex",
"indoc",
@@ -5262,7 +5263,7 @@ dependencies = [
name = "subxt-codegen"
version = "0.41.0"
dependencies = [
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"getrandom",
"heck",
"parity-scale-codec",
@@ -5285,7 +5286,7 @@ dependencies = [
"blake2",
"derive-where",
"frame-decode",
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"hashbrown 0.14.5",
"hex",
"impl-serde",
@@ -5360,7 +5361,7 @@ dependencies = [
"bitvec",
"criterion",
"frame-decode",
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"hashbrown 0.14.5",
"parity-scale-codec",
"scale-info",
@@ -5375,7 +5376,7 @@ version = "0.41.0"
dependencies = [
"derive-where",
"finito",
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"futures",
"getrandom",
"hex",
@@ -5443,7 +5444,7 @@ dependencies = [
name = "subxt-utils-fetchmetadata"
version = "0.41.0"
dependencies = [
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"hex",
"jsonrpsee",
"parity-scale-codec",
@@ -5457,7 +5458,8 @@ name = "subxt-utils-stripmetadata"
version = "0.41.0"
dependencies = [
"either",
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
+ "parity-scale-codec",
"scale-info",
]
@@ -5649,9 +5651,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.40.0"
+version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
+checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
@@ -5665,9 +5667,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.4.0"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
@@ -5935,7 +5937,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
name = "ui-tests"
version = "0.41.0"
dependencies = [
- "frame-metadata 20.0.0",
+ "frame-metadata 21.0.0",
"generate-custom-metadata",
"hex",
"parity-scale-codec",
diff --git a/Cargo.toml b/Cargo.toml
index 174bb41d50..9c502dabf0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -79,8 +79,8 @@ darling = "0.20.10"
derive-where = "1.2.7"
either = { version = "1.13.0", default-features = false }
finito = { version = "0.1.0", default-features = false }
-frame-decode = { version = "0.7.0", default-features = false }
-frame-metadata = { version = "20.0.0", default-features = false, features = ["unstable"] }
+frame-decode = { version = "0.7.1", default-features = false }
+frame-metadata = { version = "21.0.0", default-features = false, features = ["unstable"] }
futures = { version = "0.3.31", default-features = false, features = ["std"] }
getrandom = { version = "0.2", default-features = false }
hashbrown = "0.14.5"
diff --git a/codegen/src/api/mod.rs b/codegen/src/api/mod.rs
index aa7f345fd1..8ee4d857dd 100644
--- a/codegen/src/api/mod.rs
+++ b/codegen/src/api/mod.rs
@@ -9,6 +9,7 @@ mod constants;
mod custom_values;
mod errors;
mod events;
+mod pallet_view_functions;
mod runtime_apis;
mod storage;
@@ -170,12 +171,19 @@ impl RuntimeGenerator {
let errors = errors::generate_error_type_alias(&type_gen, pallet)?;
+ let view_functions = pallet_view_functions::generate_pallet_view_functions(
+ &type_gen,
+ pallet,
+ &crate_path,
+ )?;
+
Ok(quote! {
pub mod #mod_name {
use super::root_mod;
use super::#types_mod_ident;
#errors
#calls
+ #view_functions
#event
#storage_mod
#constants_mod
@@ -206,6 +214,12 @@ impl RuntimeGenerator {
.filter_map(|(pallet, pallet_mod_name)| pallet.call_ty_id().map(|_| pallet_mod_name))
.collect();
+ let pallets_with_view_functions: Vec<_> = pallets_with_mod_names
+ .iter()
+ .filter(|(pallet, _pallet_mod_name)| pallet.has_view_functions())
+ .map(|(_, pallet_mod_name)| pallet_mod_name)
+ .collect();
+
let rust_items = item_mod_ir.rust_items();
let apis_mod = runtime_apis::generate_runtime_apis(
@@ -283,6 +297,10 @@ impl RuntimeGenerator {
#apis_mod
+ pub fn view_functions() -> ViewFunctionsApi {
+ ViewFunctionsApi
+ }
+
pub fn custom() -> CustomValuesApi {
CustomValuesApi
}
@@ -316,6 +334,15 @@ impl RuntimeGenerator {
)*
}
+ pub struct ViewFunctionsApi;
+ impl ViewFunctionsApi {
+ #(
+ pub fn #pallets_with_view_functions(&self) -> #pallets_with_view_functions::view_functions::ViewFunctionsApi {
+ #pallets_with_view_functions::view_functions::ViewFunctionsApi
+ }
+ )*
+ }
+
/// check whether the metadata provided is aligned with this statically generated code.
pub fn is_codegen_valid_for(metadata: crate_path::Metadata) -> bool {
let runtime_metadata_hash = metadata
diff --git a/codegen/src/api/pallet_view_functions.rs b/codegen/src/api/pallet_view_functions.rs
new file mode 100644
index 0000000000..22a2f4a8ea
--- /dev/null
+++ b/codegen/src/api/pallet_view_functions.rs
@@ -0,0 +1,194 @@
+// Copyright 2019-2025 Parity Technologies (UK) Ltd.
+// This file is dual-licensed as Apache-2.0 or GPL-3.0.
+// see LICENSE for license details.
+
+use heck::ToUpperCamelCase as _;
+
+use crate::CodegenError;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use scale_typegen::typegen::ir::ToTokensWithSettings;
+use scale_typegen::TypeGenerator;
+use std::collections::HashSet;
+use subxt_metadata::{PalletMetadata, ViewFunctionMetadata};
+
+fn generate_pallet_view_function(
+ view_function: ViewFunctionMetadata<'_>,
+ type_gen: &TypeGenerator,
+ crate_path: &syn::Path,
+) -> Result<(TokenStream2, TokenStream2), CodegenError> {
+ let types_mod_ident = type_gen.types_mod_ident();
+
+ let view_function_name_str = view_function.name();
+ let view_function_name_ident = format_ident!("{}", view_function_name_str);
+
+ let query_id = view_function.query_id();
+ let validation_hash = view_function.hash();
+
+ let docs = view_function.docs();
+ let docs: TokenStream2 = type_gen
+ .settings()
+ .should_gen_docs
+ .then_some(quote! { #( #[doc = #docs ] )* })
+ .unwrap_or_default();
+
+ struct Input {
+ name: syn::Ident,
+ type_alias: syn::Ident,
+ type_path: TokenStream2,
+ }
+
+ let view_function_inputs: Vec = {
+ let mut unique_names = HashSet::new();
+ let mut unique_aliases = HashSet::new();
+
+ view_function
+ .inputs()
+ .enumerate()
+ .map(|(idx, input)| {
+ // These are method names, which can just be '_', but struct field names can't
+ // just be an underscore, so fix any such names we find to work in structs.
+ let mut name = input.name.trim_start_matches('_').to_string();
+ if name.is_empty() {
+ name = format!("_{}", idx);
+ }
+ while !unique_names.insert(name.clone()) {
+ name = format!("{}_param{}", name, idx);
+ }
+
+ // The alias type name is based on the name, above.
+ let mut alias = name.to_upper_camel_case();
+ // Note: name is not empty.
+ if alias.as_bytes()[0].is_ascii_digit() {
+ alias = format!("Param{}", alias);
+ }
+ while !unique_aliases.insert(alias.clone()) {
+ alias = format!("{}Param{}", alias, idx);
+ }
+
+ // Path to the actual type we'll have generated for this input.
+ let type_path = type_gen
+ .resolve_type_path(input.ty)
+ .expect("view function input type is in metadata; qed")
+ .to_token_stream(type_gen.settings());
+
+ Input {
+ name: format_ident!("{name}"),
+ type_alias: format_ident!("{alias}"),
+ type_path,
+ }
+ })
+ .collect()
+ };
+
+ let input_struct_params = view_function_inputs
+ .iter()
+ .map(|i| {
+ let arg = &i.name;
+ let ty = &i.type_alias;
+ quote!(pub #arg: #ty)
+ })
+ .collect::>();
+
+ let input_args = view_function_inputs
+ .iter()
+ .map(|i| {
+ let arg = &i.name;
+ let ty = &i.type_alias;
+ quote!(#arg: #view_function_name_ident::#ty)
+ })
+ .collect::>();
+
+ let input_type_aliases = view_function_inputs.iter().map(|i| {
+ let ty = &i.type_alias;
+ let path = &i.type_path;
+ quote!(pub type #ty = #path;)
+ });
+
+ let input_param_names = view_function_inputs.iter().map(|i| &i.name);
+
+ let output_type_path = type_gen
+ .resolve_type_path(view_function.output_ty())?
+ .to_token_stream(type_gen.settings());
+
+ let input_struct_derives = type_gen.settings().derives.default_derives();
+
+ // Define the input and output type bits.
+ let view_function_def = quote!(
+ pub mod #view_function_name_ident {
+ use super::root_mod;
+ use super::#types_mod_ident;
+
+ #input_struct_derives
+ pub struct Input {
+ #(#input_struct_params,)*
+ }
+
+ #(#input_type_aliases)*
+
+ pub mod output {
+ use super::#types_mod_ident;
+ pub type Output = #output_type_path;
+ }
+ }
+ );
+
+ // Define the getter method that will live on the `ViewFunctionApi` type.
+ let view_function_getter = quote!(
+ #docs
+ pub fn #view_function_name_ident(
+ &self,
+ #(#input_args),*
+ ) -> #crate_path::view_functions::payload::StaticPayload<
+ #view_function_name_ident::Input,
+ #view_function_name_ident::output::Output
+ > {
+ #crate_path::view_functions::payload::StaticPayload::new_static(
+ [#(#query_id,)*],
+ #view_function_name_ident::Input {
+ #(#input_param_names,)*
+ },
+ [#(#validation_hash,)*],
+ )
+ }
+ );
+
+ Ok((view_function_def, view_function_getter))
+}
+
+pub fn generate_pallet_view_functions(
+ type_gen: &TypeGenerator,
+ pallet: &PalletMetadata,
+ crate_path: &syn::Path,
+) -> Result {
+ if !pallet.has_view_functions() {
+ // If there are no view functions in this pallet, we
+ // don't generate anything.
+ return Ok(quote! {});
+ }
+
+ let view_functions: Vec<_> = pallet
+ .view_functions()
+ .map(|vf| generate_pallet_view_function(vf, type_gen, crate_path))
+ .collect::>()?;
+
+ let view_functions_defs = view_functions.iter().map(|(apis, _)| apis);
+ let view_functions_getters = view_functions.iter().map(|(_, getters)| getters);
+
+ let types_mod_ident = type_gen.types_mod_ident();
+
+ Ok(quote! {
+ pub mod view_functions {
+ use super::root_mod;
+ use super::#types_mod_ident;
+
+ pub struct ViewFunctionsApi;
+
+ impl ViewFunctionsApi {
+ #( #view_functions_getters )*
+ }
+
+ #( #view_functions_defs )*
+ }
+ })
+}
diff --git a/codegen/src/api/runtime_apis.rs b/codegen/src/api/runtime_apis.rs
index 57dd10cef9..1fe99feca5 100644
--- a/codegen/src/api/runtime_apis.rs
+++ b/codegen/src/api/runtime_apis.rs
@@ -78,7 +78,8 @@ fn generate_runtime_api(
// Generate alias for runtime type.
let ty = type_gen
.resolve_type_path(input.ty)
- .expect("runtime api input type is in metadata; qed").to_token_stream(type_gen.settings());
+ .expect("runtime api input type is in metadata; qed")
+ .to_token_stream(type_gen.settings());
let aliased_param = quote!( pub type #alias_name = #ty; );
// Structures are placed on the same level as the alias module.
diff --git a/core/src/dynamic.rs b/core/src/dynamic.rs
index bde62862a5..8f02b2d04c 100644
--- a/core/src/dynamic.rs
+++ b/core/src/dynamic.rs
@@ -28,6 +28,9 @@ pub use crate::storage::address::dynamic as storage;
// Execute runtime API function call dynamically.
pub use crate::runtime_api::payload::dynamic as runtime_api_call;
+// Execute View Function API function call dynamically.
+pub use crate::view_functions::payload::dynamic as view_function_call;
+
/// This is the result of making a dynamic request to a node. From this,
/// we can return the raw SCALE bytes that we were handed back, or we can
/// complete the decoding of the bytes into a [`DecodedValue`] type.
diff --git a/core/src/error.rs b/core/src/error.rs
index 5e5724537a..e4ca7d9bd2 100644
--- a/core/src/error.rs
+++ b/core/src/error.rs
@@ -105,6 +105,9 @@ pub enum MetadataError {
/// Runtime method not found.
#[error("Runtime method with name {0} not found")]
RuntimeMethodNotFound(String),
+ /// View Function not found.
+ #[error("View Function with query ID {} not found", hex::encode(.0))]
+ ViewFunctionNotFound([u8; 32]),
/// Call type not found in metadata.
#[error("Call type not found in pallet with index {0}")]
CallTypeNotFoundInPallet(u8),
diff --git a/core/src/lib.rs b/core/src/lib.rs
index d31dae4d20..a39d5e1899 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -36,6 +36,7 @@ pub mod runtime_api;
pub mod storage;
pub mod tx;
pub mod utils;
+pub mod view_functions;
pub use config::Config;
pub use error::Error;
diff --git a/core/src/view_functions/mod.rs b/core/src/view_functions/mod.rs
new file mode 100644
index 0000000000..3d422804fb
--- /dev/null
+++ b/core/src/view_functions/mod.rs
@@ -0,0 +1,69 @@
+// Copyright 2019-2024 Parity Technologies (UK) Ltd.
+// This file is dual-licensed as Apache-2.0 or GPL-3.0.
+// see LICENSE for license details.
+
+//! Encode View Function payloads, decode the associated values returned from them, and validate
+//! static View Function payloads.
+
+pub mod payload;
+
+use crate::error::{Error, MetadataError};
+use crate::metadata::{DecodeWithMetadata, Metadata};
+use alloc::vec::Vec;
+use payload::Payload;
+
+/// Run the validation logic against some View Function payload you'd like to use. Returns `Ok(())`
+/// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
+/// Return an error if the payload was not valid or something went wrong trying to validate it (ie
+/// the View Function in question do not exist at all)
+pub fn validate(payload: &P, metadata: &Metadata) -> Result<(), Error> {
+ let Some(static_hash) = payload.validation_hash() else {
+ return Ok(());
+ };
+
+ let view_function = metadata
+ .view_function_by_query_id(payload.query_id())
+ .ok_or_else(|| MetadataError::ViewFunctionNotFound(*payload.query_id()))?;
+ if static_hash != view_function.hash() {
+ return Err(MetadataError::IncompatibleCodegen.into());
+ }
+
+ Ok(())
+}
+
+/// The name of the Runtime API call which can execute
+pub const CALL_NAME: &str = "RuntimeViewFunction_execute_view_function";
+
+/// Encode the bytes that will be passed to the "execute_view_function" Runtime API call,
+/// to execute the View Function represented by the given payload.
+pub fn call_args(payload: &P, metadata: &Metadata) -> Result, Error> {
+ let mut call_args = Vec::with_capacity(32);
+ call_args.extend_from_slice(payload.query_id());
+
+ let mut call_arg_params = Vec::new();
+ payload.encode_args_to(metadata, &mut call_arg_params)?;
+
+ use codec::Encode;
+ call_arg_params.encode_to(&mut call_args);
+
+ Ok(call_args)
+}
+
+/// Decode the value bytes at the location given by the provided View Function payload.
+pub fn decode_value(
+ bytes: &mut &[u8],
+ payload: &P,
+ metadata: &Metadata,
+) -> Result {
+ let view_function = metadata
+ .view_function_by_query_id(payload.query_id())
+ .ok_or_else(|| MetadataError::ViewFunctionNotFound(*payload.query_id()))?;
+
+ let val = ::decode_with_metadata(
+ &mut &bytes[..],
+ view_function.output_ty(),
+ metadata,
+ )?;
+
+ Ok(val)
+}
diff --git a/core/src/view_functions/payload.rs b/core/src/view_functions/payload.rs
new file mode 100644
index 0000000000..26d2415c20
--- /dev/null
+++ b/core/src/view_functions/payload.rs
@@ -0,0 +1,153 @@
+// Copyright 2019-2024 Parity Technologies (UK) Ltd.
+// This file is dual-licensed as Apache-2.0 or GPL-3.0.
+// see LICENSE for license details.
+
+//! This module contains the trait and types used to represent
+//! View Function calls that can be made.
+
+use alloc::vec::Vec;
+use core::marker::PhantomData;
+use derive_where::derive_where;
+use scale_encode::EncodeAsFields;
+use scale_value::Composite;
+
+use crate::dynamic::DecodedValueThunk;
+use crate::error::MetadataError;
+use crate::Error;
+
+use crate::metadata::{DecodeWithMetadata, Metadata};
+
+/// This represents a View Function payload that can call into the runtime of node.
+///
+/// # Components
+///
+/// - associated return type
+///
+/// Resulting bytes of the call are interpreted into this type.
+///
+/// - query ID
+///
+/// The ID used to identify in the runtime which view function to call.
+///
+/// - encoded arguments
+///
+/// Each argument of the View Function must be scale-encoded.
+pub trait Payload {
+ /// The return type of the function call.
+ // Note: `DecodeWithMetadata` is needed to decode the function call result
+ // with the `subxt::Metadata.
+ type ReturnType: DecodeWithMetadata;
+
+ /// The payload target.
+ fn query_id(&self) -> &[u8; 32];
+
+ /// Scale encode the arguments data.
+ fn encode_args_to(&self, metadata: &Metadata, out: &mut Vec) -> Result<(), Error>;
+
+ /// Encode arguments data and return the output. This is a convenience
+ /// wrapper around [`Payload::encode_args_to`].
+ fn encode_args(&self, metadata: &Metadata) -> Result, Error> {
+ let mut v = Vec::new();
+ self.encode_args_to(metadata, &mut v)?;
+ Ok(v)
+ }
+
+ /// Returns the statically generated validation hash.
+ fn validation_hash(&self) -> Option<[u8; 32]> {
+ None
+ }
+}
+
+/// A View Function payload containing the generic argument data
+/// and interpreting the result of the call as `ReturnTy`.
+///
+/// This can be created from static values (ie those generated
+/// via the `subxt` macro) or dynamic values via [`dynamic`].
+#[derive_where(Clone, Debug, Eq, Ord, PartialEq, PartialOrd; ArgsData)]
+pub struct DefaultPayload {
+ query_id: [u8; 32],
+ args_data: ArgsData,
+ validation_hash: Option<[u8; 32]>,
+ _marker: PhantomData,
+}
+
+/// A statically generated View Function payload.
+pub type StaticPayload = DefaultPayload;
+/// A dynamic View Function payload.
+pub type DynamicPayload = DefaultPayload, DecodedValueThunk>;
+
+impl Payload
+ for DefaultPayload
+{
+ type ReturnType = ReturnTy;
+
+ fn query_id(&self) -> &[u8; 32] {
+ &self.query_id
+ }
+
+ fn encode_args_to(&self, metadata: &Metadata, out: &mut Vec) -> Result<(), Error> {
+ let view_function = metadata
+ .view_function_by_query_id(&self.query_id)
+ .ok_or(MetadataError::ViewFunctionNotFound(self.query_id))?;
+ let mut fields = view_function
+ .inputs()
+ .map(|input| scale_encode::Field::named(input.ty, &input.name));
+
+ self.args_data
+ .encode_as_fields_to(&mut fields, metadata.types(), out)?;
+
+ Ok(())
+ }
+
+ fn validation_hash(&self) -> Option<[u8; 32]> {
+ self.validation_hash
+ }
+}
+
+impl DefaultPayload {
+ /// Create a new [`DefaultPayload`] for a View Function call.
+ pub fn new(query_id: [u8; 32], args_data: ArgsData) -> Self {
+ DefaultPayload {
+ query_id,
+ args_data,
+ validation_hash: None,
+ _marker: PhantomData,
+ }
+ }
+
+ /// Create a new static [`DefaultPayload`] for a View Function call
+ /// using static function name and scale-encoded argument data.
+ ///
+ /// This is only expected to be used from codegen.
+ #[doc(hidden)]
+ pub fn new_static(
+ query_id: [u8; 32],
+ args_data: ArgsData,
+ hash: [u8; 32],
+ ) -> DefaultPayload {
+ DefaultPayload {
+ query_id,
+ args_data,
+ validation_hash: Some(hash),
+ _marker: core::marker::PhantomData,
+ }
+ }
+
+ /// Do not validate this call prior to submitting it.
+ pub fn unvalidated(self) -> Self {
+ Self {
+ validation_hash: None,
+ ..self
+ }
+ }
+
+ /// Returns the arguments data.
+ pub fn args_data(&self) -> &ArgsData {
+ &self.args_data
+ }
+}
+
+/// Create a new [`DynamicPayload`] to call a View Function.
+pub fn dynamic(query_id: [u8; 32], args_data: impl Into>) -> DynamicPayload {
+ DefaultPayload::new(query_id, args_data.into())
+}
diff --git a/metadata/src/from/v14.rs b/metadata/src/from/v14.rs
index 02c713829a..0b9ed168a2 100644
--- a/metadata/src/from/v14.rs
+++ b/metadata/src/from/v14.rs
@@ -68,7 +68,7 @@ impl TryFrom for Metadata {
error_ty: p.error.map(|e| e.ty.id),
error_variant_index,
constants: constants.collect(),
- view_functions: vec![],
+ view_functions: Default::default(),
associated_types: Default::default(),
docs: vec![],
},
diff --git a/metadata/src/from/v15.rs b/metadata/src/from/v15.rs
index ebfb623167..544e5992e5 100644
--- a/metadata/src/from/v15.rs
+++ b/metadata/src/from/v15.rs
@@ -63,7 +63,7 @@ impl TryFrom for Metadata {
error_ty: p.error.map(|e| e.ty.id),
error_variant_index,
constants: constants.collect(),
- view_functions: vec![],
+ view_functions: Default::default(),
associated_types: Default::default(),
docs: p.docs,
},
diff --git a/metadata/src/from/v16.rs b/metadata/src/from/v16.rs
index 419136d90f..085ba0624e 100644
--- a/metadata/src/from/v16.rs
+++ b/metadata/src/from/v16.rs
@@ -7,10 +7,9 @@ use super::TryFromError;
use crate::utils::variant_index::VariantIndex;
use crate::{
utils::ordered_map::OrderedMap, ArcStr, ConstantMetadata, ExtrinsicMetadata, Metadata,
- MethodParamMetadata, OuterEnumsMetadata, PalletMetadataInner, PalletViewFunctionMetadataInner,
- RuntimeApiMetadataInner, RuntimeApiMethodMetadataInner, StorageEntryMetadata,
- StorageEntryModifier, StorageEntryType, StorageHasher, StorageMetadata,
- TransactionExtensionMetadataInner,
+ MethodParamMetadata, OuterEnumsMetadata, PalletMetadataInner, RuntimeApiMetadataInner,
+ RuntimeApiMethodMetadataInner, StorageEntryMetadata, StorageEntryModifier, StorageEntryType,
+ StorageHasher, StorageMetadata, TransactionExtensionMetadataInner, ViewFunctionMetadataInner,
};
use frame_metadata::{v15, v16};
use hashbrown::HashMap;
@@ -41,10 +40,10 @@ impl TryFrom for Metadata {
let name: ArcStr = c.name.clone().into();
(name.clone(), from_constant_metadata(name, c))
});
- let view_functions = p
- .view_functions
- .into_iter()
- .map(from_view_function_metadata);
+ let view_functions = p.view_functions.into_iter().map(|v| {
+ let name: ArcStr = v.name.clone().into();
+ (name.clone(), from_view_function_metadata(name, v))
+ });
let call_variant_index = VariantIndex::build(p.calls.as_ref().map(|c| c.ty.id), &types);
let error_variant_index =
@@ -133,7 +132,11 @@ fn from_transaction_extension_metadata(
fn from_extrinsic_metadata(value: v16::ExtrinsicMetadata) -> ExtrinsicMetadata {
ExtrinsicMetadata {
supported_versions: value.versions,
- transaction_extensions_by_version: value.transaction_extensions_by_version,
+ transaction_extensions_by_version: value
+ .transaction_extensions_by_version
+ .into_iter()
+ .map(|(version, idxs)| (version, idxs.into_iter().map(|idx| idx.0).collect()))
+ .collect(),
transaction_extensions: value
.transaction_extensions
.into_iter()
@@ -241,10 +244,11 @@ fn from_runtime_api_method_metadata(
}
fn from_view_function_metadata(
+ name: ArcStr,
s: v16::PalletViewFunctionMetadata,
-) -> PalletViewFunctionMetadataInner {
- PalletViewFunctionMetadataInner {
- name: s.name,
+) -> ViewFunctionMetadataInner {
+ ViewFunctionMetadataInner {
+ name,
query_id: s.id,
inputs: s
.inputs
diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs
index 9d8427319a..db60ce1bec 100644
--- a/metadata/src/lib.rs
+++ b/metadata/src/lib.rs
@@ -216,6 +216,20 @@ impl Metadata {
})
}
+ /// Access a view function given its query ID, if any.
+ pub fn view_function_by_query_id(
+ &'_ self,
+ query_id: &[u8; 32],
+ ) -> Option> {
+ // Dev note: currently, we only have pallet view functions, and here
+ // we just do a naive thing of iterating over the pallets to find the one
+ // we're looking for. Eventually we should construct a separate map of view
+ // functions for easy querying here.
+ self.pallets()
+ .flat_map(|p| p.view_functions())
+ .find(|vf| vf.query_id() == query_id)
+ }
+
/// Returns custom user defined types
pub fn custom(&self) -> CustomMetadata<'_> {
CustomMetadata {
@@ -293,12 +307,29 @@ impl<'a> PalletMetadata<'a> {
)
}
+ /// Does this pallet have any view functions?
+ pub fn has_view_functions(&self) -> bool {
+ !self.inner.view_functions.is_empty()
+ }
+
/// Return an iterator over the View Functions in this pallet, if any.
- pub fn view_functions(&self) -> impl ExactSizeIterator- > {
+ pub fn view_functions(&self) -> impl ExactSizeIterator
- > {
self.inner
.view_functions
+ .values()
.iter()
- .map(|vf: &'a _| PalletViewFunctionMetadata {
+ .map(|vf: &'a _| ViewFunctionMetadata {
+ inner: vf,
+ types: self.types,
+ })
+ }
+
+ /// Return the view function with a given name, if any
+ pub fn view_function_by_name(&self, name: &str) -> Option> {
+ self.inner
+ .view_functions
+ .get_by_key(name)
+ .map(|vf: &'a _| ViewFunctionMetadata {
inner: vf,
types: self.types,
})
@@ -404,7 +435,7 @@ struct PalletMetadataInner {
/// Map from constant name to constant details.
constants: OrderedMap,
/// Details about each of the pallet view functions.
- view_functions: Vec,
+ view_functions: OrderedMap,
/// Mapping from associated type to type ID describing its shape.
associated_types: BTreeMap,
/// Pallet documentation.
@@ -832,41 +863,48 @@ struct RuntimeApiMethodMetadataInner {
docs: Vec,
}
-/// Metadata for the available pallet View Functions.
+/// Metadata for the available View Functions. Currently these exist only
+/// at the pallet level, but eventually they could exist at the runtime level too.
#[derive(Debug, Clone, Copy)]
-pub struct PalletViewFunctionMetadata<'a> {
- inner: &'a PalletViewFunctionMetadataInner,
+pub struct ViewFunctionMetadata<'a> {
+ inner: &'a ViewFunctionMetadataInner,
types: &'a PortableRegistry,
}
-impl PalletViewFunctionMetadata<'_> {
+impl<'a> ViewFunctionMetadata<'a> {
/// Method name.
- pub fn name(&self) -> &str {
+ pub fn name(&self) -> &'a str {
&self.inner.name
}
/// Query ID. This is used to query the function. Roughly, it is constructed by doing
/// `twox_128(pallet_name) ++ twox_128("fn_name(fnarg_types) -> return_ty")` .
- pub fn query_id(&self) -> [u8; 32] {
- self.inner.query_id
+ pub fn query_id(&self) -> &'a [u8; 32] {
+ &self.inner.query_id
}
/// Method documentation.
- pub fn docs(&self) -> &[String] {
+ pub fn docs(&self) -> &'a [String] {
&self.inner.docs
}
/// Method inputs.
- pub fn inputs(&self) -> impl ExactSizeIterator
- {
+ pub fn inputs(&self) -> impl ExactSizeIterator
- {
self.inner.inputs.iter()
}
/// Method return type.
pub fn output_ty(&self) -> u32 {
self.inner.output_ty
}
+ /// Return a hash for the method. The query ID of a view function validates it to some
+ /// degree, but only takes type _names_ into account. This hash takes into account the
+ /// actual _shape_ of each argument and the return type.
+ pub fn hash(&self) -> [u8; HASH_LEN] {
+ crate::utils::validation::get_view_function_hash(self)
+ }
}
#[derive(Debug, Clone)]
-struct PalletViewFunctionMetadataInner {
+struct ViewFunctionMetadataInner {
/// View function name.
- name: String,
+ name: ArcStr,
/// View function query ID.
query_id: [u8; 32],
/// Input types.
@@ -966,6 +1004,7 @@ impl codec::Decode for Metadata {
let metadata = match metadata.1 {
frame_metadata::RuntimeMetadata::V14(md) => md.try_into(),
frame_metadata::RuntimeMetadata::V15(md) => md.try_into(),
+ frame_metadata::RuntimeMetadata::V16(md) => md.try_into(),
_ => return Err("Cannot try_into() to Metadata: unsupported metadata version".into()),
};
diff --git a/metadata/src/utils/validation.rs b/metadata/src/utils/validation.rs
index 2237571f17..e114e27c57 100644
--- a/metadata/src/utils/validation.rs
+++ b/metadata/src/utils/validation.rs
@@ -6,8 +6,8 @@
use crate::{
CustomMetadata, CustomValueMetadata, ExtrinsicMetadata, Metadata, PalletMetadata,
- PalletViewFunctionMetadata, RuntimeApiMetadata, RuntimeApiMethodMetadata, StorageEntryMetadata,
- StorageEntryType,
+ RuntimeApiMetadata, RuntimeApiMethodMetadata, StorageEntryMetadata, StorageEntryType,
+ ViewFunctionMetadata,
};
use alloc::vec::Vec;
use hashbrown::HashMap;
@@ -406,12 +406,12 @@ pub fn get_runtime_apis_hash(trait_metadata: RuntimeApiMetadata) -> Hash {
})
}
-/// Obtain the hash of a specific pallet view function, or an error if it's not found.
-pub fn get_pallet_view_function_hash(view_function: &PalletViewFunctionMetadata) -> Hash {
+/// Obtain the hash of a specific view function, or an error if it's not found.
+pub fn get_view_function_hash(view_function: &ViewFunctionMetadata) -> Hash {
let registry = view_function.types;
// The Query ID is `twox_128(pallet_name) ++ twox_128("fn_name(fnarg_types) -> return_ty")`.
- let mut bytes = view_function.query_id();
+ let mut bytes = *view_function.query_id();
// This only takes type _names_ into account, so we beef this up by combining with actual
// type hashes, in a similar approach to runtime APIs..
@@ -439,7 +439,7 @@ fn get_pallet_view_functions_hash(pallet_metadata: &PalletMetadata) -> Hash {
// be identical regardless. For this, we can just XOR the hashes for each method
// together; we'll get the same output whichever order they are XOR'd together in,
// so long as each individual method is the same.
- xor(bytes, get_pallet_view_function_hash(&method_metadata))
+ xor(bytes, get_view_function_hash(&method_metadata))
})
}
diff --git a/subxt/src/client/offline_client.rs b/subxt/src/client/offline_client.rs
index 9f8a8f0d68..15845ef255 100644
--- a/subxt/src/client/offline_client.rs
+++ b/subxt/src/client/offline_client.rs
@@ -11,6 +11,7 @@ use crate::{
runtime_api::RuntimeApiClient,
storage::StorageClient,
tx::TxClient,
+ view_functions::ViewFunctionsClient,
Metadata,
};
@@ -67,11 +68,16 @@ pub trait OfflineClientT: Clone + Send + Sync + 'static {
BlocksClient::new(self.clone())
}
- /// Work with runtime API.
+ /// Work with runtime APIs.
fn runtime_api(&self) -> RuntimeApiClient {
RuntimeApiClient::new(self.clone())
}
+ /// Work with View Functions.
+ fn view_functions(&self) -> ViewFunctionsClient {
+ ViewFunctionsClient::new(self.clone())
+ }
+
/// Work this custom types.
fn custom_values(&self) -> CustomValuesClient {
CustomValuesClient::new(self.clone())
@@ -150,6 +156,21 @@ impl OfflineClient {
>::constants(self)
}
+ /// Work with blocks.
+ pub fn blocks(&self) -> BlocksClient {
+ >::blocks(self)
+ }
+
+ /// Work with runtime APIs.
+ pub fn runtime_api(&self) -> RuntimeApiClient {
+ >::runtime_api(self)
+ }
+
+ /// Work with View Functions.
+ pub fn view_functions(&self) -> ViewFunctionsClient {
+ >::view_functions(self)
+ }
+
/// Access custom types
pub fn custom_values(&self) -> CustomValuesClient {
>::custom_values(self)
diff --git a/subxt/src/client/online_client.rs b/subxt/src/client/online_client.rs
index 9824520fad..e5032fb16a 100644
--- a/subxt/src/client/online_client.rs
+++ b/subxt/src/client/online_client.rs
@@ -14,6 +14,7 @@ use crate::{
runtime_api::RuntimeApiClient,
storage::StorageClient,
tx::TxClient,
+ view_functions::ViewFunctionsClient,
Metadata,
};
use derive_where::derive_where;
@@ -348,11 +349,6 @@ impl OnlineClient {
>::constants(self)
}
- /// Access custom types.
- pub fn custom_values(&self) -> CustomValuesClient {
- >::custom_values(self)
- }
-
/// Work with blocks.
pub fn blocks(&self) -> BlocksClient {
>::blocks(self)
@@ -362,6 +358,16 @@ impl OnlineClient {
pub fn runtime_api(&self) -> RuntimeApiClient {
>::runtime_api(self)
}
+
+ /// Work with View Functions.
+ pub fn view_functions(&self) -> ViewFunctionsClient {
+ >::view_functions(self)
+ }
+
+ /// Access custom types.
+ pub fn custom_values(&self) -> CustomValuesClient {
+ >::custom_values(self)
+ }
}
impl OfflineClientT for OnlineClient {
diff --git a/subxt/src/lib.rs b/subxt/src/lib.rs
index 57dc44263c..57b8e02e34 100644
--- a/subxt/src/lib.rs
+++ b/subxt/src/lib.rs
@@ -49,6 +49,7 @@ pub mod runtime_api;
pub mod storage;
pub mod tx;
pub mod utils;
+pub mod view_functions;
/// This module provides a [`Config`] type, which is used to define various
/// types that are important in order to speak to a particular chain.
@@ -75,7 +76,8 @@ pub mod metadata {
/// Submit dynamic transactions.
pub mod dynamic {
pub use subxt_core::dynamic::{
- constant, runtime_api_call, storage, tx, At, DecodedValue, DecodedValueThunk, Value,
+ constant, runtime_api_call, storage, tx, view_function_call, At, DecodedValue,
+ DecodedValueThunk, Value,
};
}
diff --git a/subxt/src/view_functions/mod.rs b/subxt/src/view_functions/mod.rs
new file mode 100644
index 0000000000..c8fa159512
--- /dev/null
+++ b/subxt/src/view_functions/mod.rs
@@ -0,0 +1,14 @@
+// Copyright 2019-2025 Parity Technologies (UK) Ltd.
+// This file is dual-licensed as Apache-2.0 or GPL-3.0.
+// see LICENSE for license details.
+
+//! Types associated with executing View Function calls.
+
+mod view_function_types;
+mod view_functions_client;
+
+pub use subxt_core::view_functions::payload::{
+ dynamic, DefaultPayload, DynamicPayload, Payload, StaticPayload,
+};
+pub use view_function_types::ViewFunctionsApi;
+pub use view_functions_client::ViewFunctionsClient;
diff --git a/subxt/src/view_functions/view_function_types.rs b/subxt/src/view_functions/view_function_types.rs
new file mode 100644
index 0000000000..fef1ac47e2
--- /dev/null
+++ b/subxt/src/view_functions/view_function_types.rs
@@ -0,0 +1,79 @@
+// Copyright 2019-2025 Parity Technologies (UK) Ltd.
+// This file is dual-licensed as Apache-2.0 or GPL-3.0.
+// see LICENSE for license details.
+
+use super::Payload;
+use crate::{
+ backend::BlockRef,
+ client::OnlineClientT,
+ config::{Config, HashFor},
+ error::Error,
+};
+use derive_where::derive_where;
+use std::{future::Future, marker::PhantomData};
+
+/// Execute View Function calls.
+#[derive_where(Clone; Client)]
+pub struct ViewFunctionsApi {
+ client: Client,
+ block_ref: BlockRef>,
+ _marker: PhantomData,
+}
+
+impl ViewFunctionsApi {
+ /// Create a new [`ViewFunctionsApi`]
+ pub(crate) fn new(client: Client, block_ref: BlockRef>) -> Self {
+ Self {
+ client,
+ block_ref,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl ViewFunctionsApi
+where
+ T: Config,
+ Client: OnlineClientT,
+{
+ /// Run the validation logic against some View Function payload you'd like to use. Returns `Ok(())`
+ /// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
+ /// Return an error if the payload was not valid or something went wrong trying to validate it (ie
+ /// the View Function in question do not exist at all)
+ pub fn validate(&self, payload: &Call) -> Result<(), Error> {
+ subxt_core::view_functions::validate(payload, &self.client.metadata()).map_err(Into::into)
+ }
+
+ /// Execute a View Function call.
+ pub fn call(
+ &self,
+ payload: Call,
+ ) -> impl Future