Metadata V16: Implement support for Pallet View Functions (#1981)

* Support Pallet View Functions in Subxt

* fmt

* clippy

* Move a little view function logic to subxt_core

* clippy

* Add back check that prob isnt needed

* avoid vec macro in core

* Add view funciton test and apply various fixes to get it working

* Add test for dynamic view fn call and fix issues

* clippy

* fix test-runtime

* fmt

* remove export

* avoid vec for nostd core

* use const instead of fn for view fn call name

* Update to support latest unstable metadata

* Update metadata stripping tests for new v16 version
This commit is contained in:
James Wilson
2025-04-24 14:42:07 +01:00
committed by GitHub
parent 21b3f52191
commit 4524590821
28 changed files with 875 additions and 108 deletions
Generated
+22 -20
View File
@@ -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",
+2 -2
View File
@@ -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"
+27
View File
@@ -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
+194
View File
@@ -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<Input> = {
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::<Vec<_>>();
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::<Vec<_>>();
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<TokenStream2, CodegenError> {
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::<Result<_, _>>()?;
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 )*
}
})
}
+2 -1
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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),
+1
View File
@@ -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;
+69
View File
@@ -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<P: Payload>(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<P: Payload>(payload: &P, metadata: &Metadata) -> Result<Vec<u8>, 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<P: Payload>(
bytes: &mut &[u8],
payload: &P,
metadata: &Metadata,
) -> Result<P::ReturnType, Error> {
let view_function = metadata
.view_function_by_query_id(payload.query_id())
.ok_or_else(|| MetadataError::ViewFunctionNotFound(*payload.query_id()))?;
let val = <P::ReturnType as DecodeWithMetadata>::decode_with_metadata(
&mut &bytes[..],
view_function.output_ty(),
metadata,
)?;
Ok(val)
}
+153
View File
@@ -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<u8>) -> 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<Vec<u8>, 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<ArgsData, ReturnTy> {
query_id: [u8; 32],
args_data: ArgsData,
validation_hash: Option<[u8; 32]>,
_marker: PhantomData<ReturnTy>,
}
/// A statically generated View Function payload.
pub type StaticPayload<ArgsData, ReturnTy> = DefaultPayload<ArgsData, ReturnTy>;
/// A dynamic View Function payload.
pub type DynamicPayload = DefaultPayload<Composite<()>, DecodedValueThunk>;
impl<ArgsData: EncodeAsFields, ReturnTy: DecodeWithMetadata> Payload
for DefaultPayload<ArgsData, ReturnTy>
{
type ReturnType = ReturnTy;
fn query_id(&self) -> &[u8; 32] {
&self.query_id
}
fn encode_args_to(&self, metadata: &Metadata, out: &mut Vec<u8>) -> 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<ReturnTy, ArgsData> DefaultPayload<ArgsData, ReturnTy> {
/// 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<ArgsData, ReturnTy> {
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<Composite<()>>) -> DynamicPayload {
DefaultPayload::new(query_id, args_data.into())
}
+1 -1
View File
@@ -68,7 +68,7 @@ impl TryFrom<v14::RuntimeMetadataV14> 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![],
},
+1 -1
View File
@@ -63,7 +63,7 @@ impl TryFrom<v15::RuntimeMetadataV15> 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,
},
+16 -12
View File
@@ -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<v16::RuntimeMetadataV16> 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<PortableForm>) -> 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<PortableForm>,
) -> PalletViewFunctionMetadataInner {
PalletViewFunctionMetadataInner {
name: s.name,
) -> ViewFunctionMetadataInner {
ViewFunctionMetadataInner {
name,
query_id: s.id,
inputs: s
.inputs
+53 -14
View File
@@ -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<ViewFunctionMetadata<'_>> {
// 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<Item = PalletViewFunctionMetadata<'a>> {
pub fn view_functions(&self) -> impl ExactSizeIterator<Item = ViewFunctionMetadata<'a>> {
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<ViewFunctionMetadata<'a>> {
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<ArcStr, ConstantMetadata>,
/// Details about each of the pallet view functions.
view_functions: Vec<PalletViewFunctionMetadataInner>,
view_functions: OrderedMap<ArcStr, ViewFunctionMetadataInner>,
/// Mapping from associated type to type ID describing its shape.
associated_types: BTreeMap<String, u32>,
/// Pallet documentation.
@@ -832,41 +863,48 @@ struct RuntimeApiMethodMetadataInner {
docs: Vec<String>,
}
/// 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<Item = &MethodParamMetadata> {
pub fn inputs(&self) -> impl ExactSizeIterator<Item = &'a MethodParamMetadata> {
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()),
};
+6 -6
View File
@@ -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))
})
}
+22 -1
View File
@@ -11,6 +11,7 @@ use crate::{
runtime_api::RuntimeApiClient,
storage::StorageClient,
tx::TxClient,
view_functions::ViewFunctionsClient,
Metadata,
};
@@ -67,11 +68,16 @@ pub trait OfflineClientT<T: Config>: Clone + Send + Sync + 'static {
BlocksClient::new(self.clone())
}
/// Work with runtime API.
/// Work with runtime APIs.
fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
RuntimeApiClient::new(self.clone())
}
/// Work with View Functions.
fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
ViewFunctionsClient::new(self.clone())
}
/// Work this custom types.
fn custom_values(&self) -> CustomValuesClient<T, Self> {
CustomValuesClient::new(self.clone())
@@ -150,6 +156,21 @@ impl<T: Config> OfflineClient<T> {
<Self as OfflineClientT<T>>::constants(self)
}
/// Work with blocks.
pub fn blocks(&self) -> BlocksClient<T, Self> {
<Self as OfflineClientT<T>>::blocks(self)
}
/// Work with runtime APIs.
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
<Self as OfflineClientT<T>>::runtime_api(self)
}
/// Work with View Functions.
pub fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
<Self as OfflineClientT<T>>::view_functions(self)
}
/// Access custom types
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
<Self as OfflineClientT<T>>::custom_values(self)
+11 -5
View File
@@ -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<T: Config> OnlineClient<T> {
<Self as OfflineClientT<T>>::constants(self)
}
/// Access custom types.
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
<Self as OfflineClientT<T>>::custom_values(self)
}
/// Work with blocks.
pub fn blocks(&self) -> BlocksClient<T, Self> {
<Self as OfflineClientT<T>>::blocks(self)
@@ -362,6 +358,16 @@ impl<T: Config> OnlineClient<T> {
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
<Self as OfflineClientT<T>>::runtime_api(self)
}
/// Work with View Functions.
pub fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
<Self as OfflineClientT<T>>::view_functions(self)
}
/// Access custom types.
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
<Self as OfflineClientT<T>>::custom_values(self)
}
}
impl<T: Config> OfflineClientT<T> for OnlineClient<T> {
+3 -1
View File
@@ -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,
};
}
+14
View File
@@ -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;
@@ -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<T: Config, Client> {
client: Client,
block_ref: BlockRef<HashFor<T>>,
_marker: PhantomData<T>,
}
impl<T: Config, Client> ViewFunctionsApi<T, Client> {
/// Create a new [`ViewFunctionsApi`]
pub(crate) fn new(client: Client, block_ref: BlockRef<HashFor<T>>) -> Self {
Self {
client,
block_ref,
_marker: PhantomData,
}
}
}
impl<T, Client> ViewFunctionsApi<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// 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<Call: Payload>(&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<Call: Payload>(
&self,
payload: Call,
) -> impl Future<Output = Result<Call::ReturnType, Error>> {
let client = self.client.clone();
let block_hash = self.block_ref.hash();
// Ensure that the returned future doesn't have a lifetime tied to api.view_functions(),
// which is a temporary thing we'll be throwing away quickly:
async move {
let metadata = client.metadata();
// Validate the View Function payload hash against the compile hash from codegen.
subxt_core::view_functions::validate(&payload, &metadata)?;
// Assemble the data to call the "execute_view_function" runtime API, which
// then calls the relevant view function.
let call_name = subxt_core::view_functions::CALL_NAME;
let call_args = subxt_core::view_functions::call_args(&payload, &metadata)?;
// Make the call.
let bytes = client
.backend()
.call(call_name, Some(call_args.as_slice()), block_hash)
.await?;
// Decode the response.
let value =
subxt_core::view_functions::decode_value(&mut &*bytes, &payload, &metadata)?;
Ok(value)
}
}
}
@@ -0,0 +1,57 @@
// 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::view_function_types::ViewFunctionsApi;
use crate::{
backend::BlockRef,
client::OnlineClientT,
config::{Config, HashFor},
error::Error,
};
use derive_where::derive_where;
use std::{future::Future, marker::PhantomData};
/// Make View Function calls at some block.
#[derive_where(Clone; Client)]
pub struct ViewFunctionsClient<T, Client> {
client: Client,
_marker: PhantomData<T>,
}
impl<T, Client> ViewFunctionsClient<T, Client> {
/// Create a new [`ViewFunctionsClient`]
pub fn new(client: Client) -> Self {
Self {
client,
_marker: PhantomData,
}
}
}
impl<T, Client> ViewFunctionsClient<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Obtain an interface to call View Functions at some block hash.
pub fn at(&self, block_ref: impl Into<BlockRef<HashFor<T>>>) -> ViewFunctionsApi<T, Client> {
ViewFunctionsApi::new(self.client.clone(), block_ref.into())
}
/// Obtain an interface to call View Functions at the latest block hash.
pub fn at_latest(
&self,
) -> impl Future<Output = Result<ViewFunctionsApi<T, Client>, Error>> + Send + 'static {
// Clone and pass the client in like this so that we can explicitly
// return a Future that's Send + 'static, rather than tied to &self.
let client = self.client.clone();
async move {
// get the ref for the latest finalized block and use that.
let block_ref = client.backend().latest_finalized_block_ref().await?;
Ok(ViewFunctionsApi::new(client, block_ref))
}
}
}
+1
View File
@@ -35,6 +35,7 @@ hex = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
scale-info = { workspace = true, features = ["bit-vec"] }
scale-value = { workspace = true }
sp-core = { workspace = true, features = ["std"] }
syn = { workspace = true }
subxt = { workspace = true, features = ["unstable-metadata", "native", "jsonrpsee", "reconnecting-rpc-client"] }
@@ -7,6 +7,7 @@ mod client;
mod codegen;
mod frame;
mod metadata_validation;
mod pallet_view_functions;
mod runtime_api;
mod storage;
mod transactions;
@@ -0,0 +1,60 @@
use crate::{subxt_test, test_context};
use test_runtime::node_runtime_unstable;
#[subxt_test]
async fn call_view_function() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
use node_runtime_unstable::proxy::view_functions::check_permissions::{Call, ProxyType};
// This is one of only two view functions that currently exists at the time of writing.
let call = Call::System(node_runtime_unstable::system::Call::remark {
remark: b"hi".to_vec(),
});
let proxy_type = ProxyType::Any;
let view_function_call = node_runtime_unstable::view_functions()
.proxy()
.check_permissions(call, proxy_type);
// Submit the call and get back a result.
let _is_call_allowed = api
.view_functions()
.at_latest()
.await?
.call(view_function_call)
.await?;
Ok(())
}
#[subxt_test]
async fn call_view_function_dynamically() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let metadata = api.metadata();
let query_id = metadata
.pallet_by_name("Proxy")
.unwrap()
.view_function_by_name("check_permissions")
.unwrap()
.query_id();
use scale_value::value;
let view_function_call = subxt::dynamic::view_function_call(
*query_id,
vec![value!(System(remark(b"hi".to_vec()))), value!(Any())],
);
// Submit the call and get back a result.
let _is_call_allowed = api
.view_functions()
.at_latest()
.await?
.call(view_function_call)
.await?;
Ok(())
}
+66 -38
View File
@@ -9,6 +9,9 @@ use substrate_runner::{Error as SubstrateNodeError, SubstrateNode};
// This variable accepts a single binary name or comma separated list.
static SUBSTRATE_BIN_ENV_VAR: &str = "SUBSTRATE_NODE_PATH";
const V15_METADATA_VERSION: u32 = 15;
const UNSTABLE_METADATA_VERSION: u32 = u32::MAX;
#[tokio::main]
async fn main() {
run().await;
@@ -37,11 +40,62 @@ async fn run() {
};
let port = node.ws_port();
let out_dir_env_var = env::var_os("OUT_DIR");
let out_dir = out_dir_env_var.as_ref().unwrap().to_str().unwrap();
// Download metadata from binary. Avoid Subxt dep on `subxt::rpc::types::Bytes`and just impl here.
// This may at least prevent this script from running so often (ie whenever we change Subxt).
const V15_METADATA_VERSION: u32 = 15;
let bytes = V15_METADATA_VERSION.encode();
let (stable_metadata_path, unstable_metadata_path) = tokio::join!(
download_and_save_metadata(V15_METADATA_VERSION, port, out_dir, "v15"),
download_and_save_metadata(UNSTABLE_METADATA_VERSION, port, out_dir, "unstable")
);
// Write out our expression to generate the runtime API to a file. Ideally, we'd just write this code
// in lib.rs, but we must pass a string literal (and not `concat!(..)`) as an arg to `runtime_metadata_path`,
// and so we need to spit it out here and include it verbatim instead.
let runtime_api_contents = format!(
r#"
/// Generated types for the locally running Substrate node using V15 metadata.
#[subxt::subxt(
runtime_metadata_path = "{stable_metadata_path}",
derive_for_all_types = "Eq, PartialEq",
)]
pub mod node_runtime {{}}
/// Generated types for the locally running Substrate node using the unstable metadata.
#[subxt::subxt(
runtime_metadata_path = "{unstable_metadata_path}",
derive_for_all_types = "Eq, PartialEq",
)]
pub mod node_runtime_unstable {{}}
"#
);
let runtime_path = Path::new(&out_dir).join("runtime.rs");
fs::write(runtime_path, runtime_api_contents).expect("Couldn't write runtime rust output");
for substrate_node_path in substrate_bins_vec {
let Ok(full_path) = which::which(substrate_node_path) else {
continue;
};
// Re-build if the substrate binary we're pointed to changes (mtime):
println!("cargo:rerun-if-changed={}", full_path.to_string_lossy());
}
// Re-build if we point to a different substrate binary:
println!("cargo:rerun-if-env-changed={SUBSTRATE_BIN_ENV_VAR}");
// Re-build if this file changes:
println!("cargo:rerun-if-changed=build.rs");
}
// Download metadata from binary. Avoid Subxt dep on `subxt::rpc::types::Bytes`and just impl here.
// This may at least prevent this script from running so often (ie whenever we change Subxt).
async fn download_and_save_metadata(
version: u32,
port: u16,
out_dir: &str,
suffix: &str,
) -> String {
// Download it:
let bytes = version.encode();
let version: String = format!("0x{}", hex::encode(&bytes));
let raw: String = {
use client::ClientT;
@@ -61,42 +115,16 @@ async fn run() {
.unwrap_or_else(|e| panic!("Failed to decode metadata bytes: {e}"));
let metadata_bytes = bytes.expect("Metadata version not found");
// Save metadata to a file:
let out_dir = env::var_os("OUT_DIR").unwrap();
let metadata_path = Path::new(&out_dir).join("test_node_runtime_metadata.scale");
// Save it to a file:
let metadata_path =
Path::new(&out_dir).join(format!("test_node_runtime_metadata_{suffix}.scale"));
fs::write(&metadata_path, metadata_bytes).expect("Couldn't write metadata output");
// Write out our expression to generate the runtime API to a file. Ideally, we'd just write this code
// in lib.rs, but we must pass a string literal (and not `concat!(..)`) as an arg to `runtime_metadata_path`,
// and so we need to spit it out here and include it verbatim instead.
let runtime_api_contents = format!(
r#"
#[subxt::subxt(
runtime_metadata_path = "{}",
derive_for_all_types = "Eq, PartialEq",
)]
pub mod node_runtime {{}}
"#,
metadata_path
.to_str()
.expect("Path to metadata should be stringifiable")
);
let runtime_path = Path::new(&out_dir).join("runtime.rs");
fs::write(runtime_path, runtime_api_contents).expect("Couldn't write runtime rust output");
for substrate_node_path in substrate_bins_vec {
let Ok(full_path) = which::which(substrate_node_path) else {
continue;
};
// Re-build if the substrate binary we're pointed to changes (mtime):
println!("cargo:rerun-if-changed={}", full_path.to_string_lossy());
}
// Re-build if we point to a different substrate binary:
println!("cargo:rerun-if-env-changed={SUBSTRATE_BIN_ENV_VAR}");
// Re-build if this file changes:
println!("cargo:rerun-if-changed=build.rs");
// Convert path to string because we need this to interpolate into string
metadata_path
.to_str()
.expect("Path to metadata should be stringifiable")
.to_owned()
}
// Use jsonrpsee to obtain metadata from the node.
+1 -1
View File
@@ -7,7 +7,7 @@
/// The SCALE encoded metadata obtained from a local run of a substrate node.
pub static METADATA: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/test_node_runtime_metadata.scale"
"/test_node_runtime_metadata_v15.scale"
));
include!(concat!(env!("OUT_DIR"), "/runtime.rs"));
+1
View File
@@ -14,6 +14,7 @@ homepage.workspace = true
description = "subxt utility to strip metadata"
[dependencies]
codec = { workspace = true }
frame-metadata = { workspace = true, features = ["std"] }
scale-info = { workspace = true, features = ["std"] }
either = { workspace = true }
+6 -5
View File
@@ -439,6 +439,7 @@ mod test {
use std::collections::BTreeMap;
use super::*;
use codec::Compact;
use scale_info::meta_type;
/// Create dummy types that we can check the presense of with is_in_types.
@@ -817,7 +818,7 @@ mod test {
view_functions: vec![v16::PalletViewFunctionMetadata {
name: "some_view_function",
id: [0; 32],
inputs: vec![v16::PalletViewFunctionParamMetadata {
inputs: vec![v16::FunctionParamMetadata {
name: "input1",
ty: meta_type::<F>(),
}],
@@ -842,12 +843,12 @@ mod test {
let runtime_apis = vec![
v16::RuntimeApiMetadata {
name: "SomeApi",
version: 2,
version: Compact(2),
docs: vec![],
deprecation_info: v16::DeprecationStatus::NotDeprecated,
methods: vec![v16::RuntimeApiMethodMetadata {
name: "some_method",
inputs: vec![v16::RuntimeApiMethodParamMetadata {
inputs: vec![v16::FunctionParamMetadata {
name: "input1",
ty: meta_type::<J>(),
}],
@@ -858,12 +859,12 @@ mod test {
},
v16::RuntimeApiMetadata {
name: "AnotherApi",
version: 1,
version: Compact(1),
docs: vec![],
deprecation_info: v16::DeprecationStatus::NotDeprecated,
methods: vec![v16::RuntimeApiMethodMetadata {
name: "another_method",
inputs: vec![v16::RuntimeApiMethodParamMetadata {
inputs: vec![v16::FunctionParamMetadata {
name: "input1",
ty: meta_type::<L>(),
}],