Metadata V15: Generate Runtime APIs (#918)

* Update frame-metadata to v15.1.0

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Enable V15 unstable metadata in frame-metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Move validation hashing to dedicated file

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Use sp-metadata-ir from substrate to work with metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Revert using sp-metadata-ir in favor of conversion to v15

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Convert v14 to v15

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Use v15 for validation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Use v15 for codegen

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata/bench: Use v15

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust to v15 metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust testing

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Improve documentation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* force CI

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc: Fetch metadata at version

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* artifacts: Update polkadot.scale from commit 6dc9e84dde2

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Fetch V15 using the new API

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Add runtime API interface

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Hash runtime API metadata for validation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Extract runtime API metadata wrapper from subxt::Metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Adjust hashing cache to reflect root+item keys

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc: Add raw state_call API method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* runtime_api: Add payload with static and dynamic variants

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Allow payloads to call into the runtime

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Add example to make a runtime API call both static and dynamic

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update polkadot.rs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Simplify client fetching

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Address feedback and fallback to old API if needed

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* runtime_api: Make mutability conditional on input params

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Regenerate polkadot.rs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Retain only pallets without runtime API info

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Retry via `Metadata_metadata` without conversion

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* payload: Remove `Decode` and change validation fn

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Retain runtime API types

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Runtime APIs documentation based on flag

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update examples/examples/custom_metadata_url.rs

Co-authored-by: James Wilson <james@jsdw.me>

* Update artifacts from polkadot-a6cfdb16e9

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update polkadot.rs with polkadot-a6cfdb16e9

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Generate input structures for runtime API

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* runtime_api: Remove the static paylaod and use single impl

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Fetch account nonce

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Adjust build script to fetch latest metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Check account nonce from runtime API

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update cargo.lock

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Fix doc generation for runtime types

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Rename `inputs` runtime calls module to `types`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Generate Calls structs inside the types module

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Check Alice account nonce before submitting the tx

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* cli: Add metadata version option flag supporting v14 and unstable

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* cli: Specify version to fetch

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Fallback to fetching latest stable metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add unstable-metadata feature to fetch the latest

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* RuntimeVersion with Latest and Version(u32)

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update polkadot.rs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Adjust fetch_metadata to inspect version list

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Adjust metadata to metadata_legacy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* events: Adjust docs to use metadata_legacy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* have a pass over fetch_metadata

* cargo fmt

* Option<String> when fetch metadata via latest API

* clippy

* fmt

* cli: Use the MetadataVersion from codegen

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* cli: Specify latest as default for MetadataVersion

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* cli: Remove version from metadata and use the one from file_or_url

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Fix clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Decode metadata independently for different RPC calls

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: James Wilson <james@jsdw.me>
This commit is contained in:
Alexandru Vasile
2023-05-03 17:31:27 +03:00
committed by GitHub
parent f4eb80e78d
commit 432e856c37
33 changed files with 13959 additions and 8390 deletions
Generated
+2
View File
@@ -3692,8 +3692,10 @@ dependencies = [
name = "test-runtime"
version = "0.28.0"
dependencies = [
"hex",
"impl-serde",
"jsonrpsee",
"parity-scale-codec",
"serde",
"substrate-runner",
"subxt",
Binary file not shown.
+31 -8
View File
@@ -11,6 +11,7 @@ use frame_metadata::{
use jsonrpsee::client_transport::ws::Uri;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use subxt_codegen::utils::MetadataVersion;
use subxt_metadata::{get_metadata_hash, get_pallet_hash, metadata_v14_to_latest};
/// Verify metadata compatibility between substrate nodes.
@@ -25,16 +26,35 @@ pub struct Opts {
/// The validation will omit the full metadata check and focus instead on the pallet.
#[clap(long, value_parser)]
pallet: Option<String>,
/// Specify the metadata version.
///
/// - unstable:
///
/// Use the latest unstable metadata of the node.
///
/// - number
///
/// Use this specific metadata version.
///
/// Defaults to latest.
#[clap(long = "version", default_value = "latest")]
version: MetadataVersion,
}
pub async fn run(opts: Opts) -> color_eyre::Result<()> {
match opts.pallet {
Some(pallet) => handle_pallet_metadata(opts.nodes.as_slice(), pallet.as_str()).await,
None => handle_full_metadata(opts.nodes.as_slice()).await,
Some(pallet) => {
handle_pallet_metadata(opts.nodes.as_slice(), pallet.as_str(), opts.version).await
}
None => handle_full_metadata(opts.nodes.as_slice(), opts.version).await,
}
}
async fn handle_pallet_metadata(nodes: &[Uri], name: &str) -> color_eyre::Result<()> {
async fn handle_pallet_metadata(
nodes: &[Uri],
name: &str,
version: MetadataVersion,
) -> color_eyre::Result<()> {
#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct CompatibilityPallet {
@@ -44,7 +64,7 @@ async fn handle_pallet_metadata(nodes: &[Uri], name: &str) -> color_eyre::Result
let mut compatibility: CompatibilityPallet = Default::default();
for node in nodes.iter() {
let metadata = fetch_runtime_metadata(node).await?;
let metadata = fetch_runtime_metadata(node, version).await?;
match metadata.pallets.iter().find(|pallet| pallet.name == name) {
Some(pallet_metadata) => {
@@ -73,10 +93,10 @@ async fn handle_pallet_metadata(nodes: &[Uri], name: &str) -> color_eyre::Result
Ok(())
}
async fn handle_full_metadata(nodes: &[Uri]) -> color_eyre::Result<()> {
async fn handle_full_metadata(nodes: &[Uri], version: MetadataVersion) -> color_eyre::Result<()> {
let mut compatibility_map: HashMap<String, Vec<String>> = HashMap::new();
for node in nodes.iter() {
let metadata = fetch_runtime_metadata(node).await?;
let metadata = fetch_runtime_metadata(node, version).await?;
let hash = get_metadata_hash(&metadata);
let hex_hash = hex::encode(hash);
println!("Node {node:?} has metadata hash {hex_hash:?}",);
@@ -96,8 +116,11 @@ async fn handle_full_metadata(nodes: &[Uri]) -> color_eyre::Result<()> {
Ok(())
}
async fn fetch_runtime_metadata(url: &Uri) -> color_eyre::Result<RuntimeMetadataV15> {
let bytes = subxt_codegen::utils::fetch_metadata_bytes(url).await?;
async fn fetch_runtime_metadata(
url: &Uri,
version: MetadataVersion,
) -> color_eyre::Result<RuntimeMetadataV15> {
let bytes = subxt_codegen::utils::fetch_metadata_bytes(url, version).await?;
let metadata = <RuntimeMetadataPrefixed as Decode>::decode(&mut &bytes[..])?;
if metadata.0 != META_RESERVED {
+35 -7
View File
@@ -5,7 +5,7 @@
use clap::Args;
use color_eyre::eyre;
use std::{fs, io::Read, path::PathBuf};
use subxt_codegen::utils::Uri;
use subxt_codegen::utils::{MetadataVersion, Uri};
/// The source of the metadata.
#[derive(Debug, Args)]
@@ -16,29 +16,57 @@ pub struct FileOrUrl {
/// The path to the encoded metadata file.
#[clap(long, value_parser)]
file: Option<PathBuf>,
/// Specify the metadata version.
///
/// - unstable:
///
/// Use the latest unstable metadata of the node.
///
/// - number
///
/// Use this specific metadata version.
///
/// Defaults to 14.
#[clap(long)]
version: Option<MetadataVersion>,
}
impl FileOrUrl {
/// Fetch the metadata bytes.
pub async fn fetch(&self) -> color_eyre::Result<Vec<u8>> {
match (&self.file, &self.url) {
match (&self.file, &self.url, self.version) {
// Can't provide both --file and --url
(Some(_), Some(_)) => {
(Some(_), Some(_), _) => {
eyre::bail!("specify one of `--url` or `--file` but not both")
}
// Load from --file path
(Some(path), None) => {
(Some(path), None, None) => {
let mut file = fs::File::open(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
// Cannot load the metadata from the file and specify a version to fetch.
(Some(_), None, Some(_)) => {
// Note: we could provide the ability to convert between metadata versions
// but that would be involved because we'd need to convert
// from each metadata to the latest one and from the
// latest one to each metadata version. For now, disable the conversion.
eyre::bail!("`--file` is incompatible with `--version`")
}
// Fetch from --url
(None, Some(uri)) => Ok(subxt_codegen::utils::fetch_metadata_bytes(uri).await?),
(None, Some(uri), version) => Ok(subxt_codegen::utils::fetch_metadata_bytes(
uri,
version.unwrap_or_default(),
)
.await?),
// Default if neither is provided; fetch from local url
(None, None) => {
(None, None, version) => {
let uri = Uri::from_static("http://localhost:9933");
Ok(subxt_codegen::utils::fetch_metadata_bytes(&uri).await?)
Ok(
subxt_codegen::utils::fetch_metadata_bytes(&uri, version.unwrap_or_default())
.await?,
)
}
}
}
+7 -3
View File
@@ -90,11 +90,11 @@ pub fn generate_calls(
pub fn #fn_name(
&self,
#( #call_fn_args, )*
) -> #crate_path::tx::Payload<#struct_name> {
) -> #crate_path::tx::Payload<types::#struct_name> {
#crate_path::tx::Payload::new_static(
#pallet_name,
#call_name,
#struct_name { #( #call_args, )* },
types::#struct_name { #( #call_args, )* },
[#(#call_hash,)*]
)
}
@@ -120,7 +120,11 @@ pub fn generate_calls(
type DispatchError = #types_mod_ident::sp_runtime::DispatchError;
#( #call_structs )*
pub mod types {
use super::#types_mod_ident;
#( #call_structs )*
}
pub struct TransactionApi;
+21 -2
View File
@@ -8,6 +8,7 @@ mod calls;
mod constants;
mod errors;
mod events;
mod runtime_apis;
mod storage;
use frame_metadata::v15::RuntimeMetadataV15;
@@ -18,7 +19,7 @@ use crate::error::CodegenError;
use crate::{
ir,
types::{CompositeDef, CompositeDefFields, TypeGenerator, TypeSubstitutes},
utils::{fetch_metadata_bytes_blocking, Uri},
utils::{fetch_metadata_bytes_blocking, MetadataVersion, Uri},
CratePath,
};
use codec::Decode;
@@ -95,7 +96,11 @@ pub fn generate_runtime_api_from_url(
should_gen_docs: bool,
runtime_types_only: bool,
) -> Result<TokenStream2, CodegenError> {
let bytes = fetch_metadata_bytes_blocking(url)?;
// Fetch latest unstable version, if that fails fall back to the latest stable.
let bytes = match fetch_metadata_bytes_blocking(url, MetadataVersion::Unstable) {
Ok(bytes) => bytes,
Err(_) => fetch_metadata_bytes_blocking(url, MetadataVersion::Latest)?,
};
generate_runtime_api_from_bytes(
item_mod,
@@ -434,6 +439,14 @@ impl RuntimeGenerator {
let rust_items = item_mod_ir.rust_items();
let apis_mod = runtime_apis::generate_runtime_apis(
&self.metadata,
&type_gen,
types_mod_ident,
&crate_path,
should_gen_docs,
)?;
Ok(quote! {
#( #item_mod_attrs )*
#[allow(dead_code, unused_imports, non_camel_case_types)]
@@ -487,6 +500,12 @@ impl RuntimeGenerator {
TransactionApi
}
pub fn apis() -> runtime_apis::RuntimeApi {
runtime_apis::RuntimeApi
}
#apis_mod
pub struct ConstantsApi;
impl ConstantsApi {
#(
+168
View File
@@ -0,0 +1,168 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{types::TypeGenerator, CodegenError, CratePath};
use frame_metadata::v15::{RuntimeApiMetadata, RuntimeMetadataV15};
use heck::ToSnakeCase as _;
use heck::ToUpperCamelCase as _;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use scale_info::form::PortableForm;
/// Generates runtime functions for the given API metadata.
fn generate_runtime_api(
metadata: &RuntimeMetadataV15,
api: &RuntimeApiMetadata<PortableForm>,
type_gen: &TypeGenerator,
types_mod_ident: &syn::Ident,
crate_path: &CratePath,
should_gen_docs: bool,
) -> Result<(TokenStream2, TokenStream2), CodegenError> {
// Trait name must remain as is (upper case) to identity the runtime call.
let trait_name = &api.name;
// The snake case for the trait name.
let trait_name_snake = format_ident!("{}", api.name.to_snake_case());
let docs = &api.docs;
let docs: TokenStream2 = should_gen_docs
.then_some(quote! { #( #[doc = #docs ] )* })
.unwrap_or_default();
let structs_and_methods: Vec<_> = api.methods.iter().map(|method| {
let method_name = format_ident!("{}", method.name);
// Runtime function name is `TraitName_MethodName`.
let runtime_fn_name = format!("{}_{}", trait_name, method_name);
let docs = &method.docs;
let docs: TokenStream2 = should_gen_docs
.then_some(quote! { #( #[doc = #docs ] )* })
.unwrap_or_default();
let inputs: Vec<_> = method.inputs.iter().map(|input| {
let name = format_ident!("{}", &input.name);
let ty = type_gen.resolve_type_path(input.ty.id);
let param = quote!(#name: #ty);
(param, name)
}).collect();
let params = inputs.iter().map(|(param, _)| param);
let param_names = inputs.iter().map(|(_, name)| name);
// From the method metadata generate a structure that holds
// all parameter types. This structure is used with metadata
// to encode parameters to the call via `encode_as_fields_to`.
let derives = type_gen.default_derives();
let struct_name = format_ident!("{}", method.name.to_upper_camel_case());
let struct_params = params.clone();
let struct_input = quote!(
#derives
pub struct #struct_name {
#( pub #struct_params, )*
}
);
let output = type_gen.resolve_type_path(method.output.id);
let Ok(call_hash) =
subxt_metadata::get_runtime_api_hash(metadata, trait_name, &method.name) else {
return Err(CodegenError::MissingRuntimeApiMetadata(
trait_name.into(),
method.name.clone(),
))
};
let method = quote!(
#docs
pub fn #method_name(&self, #( #params, )* ) -> #crate_path::runtime_api::Payload<types::#struct_name, #output> {
#crate_path::runtime_api::Payload::new_static(
#runtime_fn_name,
types::#struct_name { #( #param_names, )* },
[#(#call_hash,)*],
)
}
);
Ok((struct_input, method))
}).collect::<Result<_, _>>()?;
let trait_name = format_ident!("{}", trait_name);
let structs = structs_and_methods.iter().map(|(struct_, _)| struct_);
let methods = structs_and_methods.iter().map(|(_, method)| method);
let runtime_api = quote!(
pub mod #trait_name_snake {
use super::root_mod;
use super::#types_mod_ident;
#docs
pub struct #trait_name;
impl #trait_name {
#( #methods )*
}
pub mod types {
use super::#types_mod_ident;
#( #structs )*
}
}
);
// A getter for the `RuntimeApi` to get the trait structure.
let trait_getter = quote!(
pub fn #trait_name_snake(&self) -> #trait_name_snake::#trait_name {
#trait_name_snake::#trait_name
}
);
Ok((runtime_api, trait_getter))
}
/// Generate the runtime APIs.
pub fn generate_runtime_apis(
metadata: &RuntimeMetadataV15,
type_gen: &TypeGenerator,
types_mod_ident: &syn::Ident,
crate_path: &CratePath,
should_gen_docs: bool,
) -> Result<TokenStream2, CodegenError> {
let apis = &metadata.apis;
let runtime_fns: Vec<_> = apis
.iter()
.map(|api| {
generate_runtime_api(
metadata,
api,
type_gen,
types_mod_ident,
crate_path,
should_gen_docs,
)
})
.collect::<Result<_, _>>()?;
let runtime_apis_def = runtime_fns.iter().map(|(apis, _)| apis);
let runtime_apis_getters = runtime_fns.iter().map(|(_, getters)| getters);
Ok(quote! {
pub mod runtime_apis {
use super::root_mod;
use super::#types_mod_ident;
use #crate_path::ext::codec::Encode;
pub struct RuntimeApi;
impl RuntimeApi {
#( #runtime_apis_getters )*
}
#( #runtime_apis_def )*
}
})
}
+7
View File
@@ -42,6 +42,9 @@ pub enum CodegenError {
/// Metadata for call could not be found.
#[error("Metadata for call entry {0}_{1} could not be found. Make sure you are providing a valid substrate-based metadata")]
MissingCallMetadata(String, String),
/// Metadata for call could not be found.
#[error("Metadata for runtime API entry {0}_{1} could not be found. Make sure you are providing a valid substrate-based metadata")]
MissingRuntimeApiMetadata(String, String),
/// Call variant must have all named fields.
#[error("Call variant for type {0} must have all named fields. Make sure you are providing a valid substrate-based metadata")]
InvalidCallVariant(u32),
@@ -77,10 +80,14 @@ impl CodegenError {
pub enum FetchMetadataError {
#[error("Cannot decode hex value: {0}")]
DecodeError(#[from] hex::FromHexError),
#[error("Cannot scale encode/decode value: {0}")]
CodecError(#[from] codec::Error),
#[error("Request error: {0}")]
RequestError(#[from] jsonrpsee::core::Error),
#[error("'{0}' not supported, supported URI schemes are http, https, ws or wss.")]
InvalidScheme(String),
#[error("Other error: {0}")]
Other(String),
}
/// Error attempting to do type substitution.
+178 -19
View File
@@ -3,6 +3,7 @@
// see LICENSE for license details.
use crate::error::FetchMetadataError;
use codec::{Decode, Encode};
use jsonrpsee::{
async_client::ClientBuilder,
client_transport::ws::{Uri, WsTransportClientBuilder},
@@ -12,14 +13,51 @@ use jsonrpsee::{
};
use std::time::Duration;
/// The metadata version that is fetched from the node.
#[derive(Default, Debug, Clone, Copy)]
pub enum MetadataVersion {
/// Latest stable version of the metadata.
#[default]
Latest,
/// Fetch a specified version of the metadata.
Version(u32),
/// Latest unstable version of the metadata.
Unstable,
}
// Note: Implementation needed for the CLI tool.
impl std::str::FromStr for MetadataVersion {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"unstable" => Ok(MetadataVersion::Unstable),
"latest" => Ok(MetadataVersion::Latest),
version => {
let num: u32 = version
.parse()
.map_err(|_| format!("Invalid metadata version specified {:?}", version))?;
Ok(MetadataVersion::Version(num))
}
}
}
}
/// Returns the metadata bytes from the provided URL, blocking the current thread.
pub fn fetch_metadata_bytes_blocking(url: &Uri) -> Result<Vec<u8>, FetchMetadataError> {
tokio_block_on(fetch_metadata_bytes(url))
pub fn fetch_metadata_bytes_blocking(
url: &Uri,
version: MetadataVersion,
) -> Result<Vec<u8>, FetchMetadataError> {
tokio_block_on(fetch_metadata_bytes(url, version))
}
/// Returns the raw, 0x prefixed metadata hex from the provided URL, blocking the current thread.
pub fn fetch_metadata_hex_blocking(url: &Uri) -> Result<String, FetchMetadataError> {
tokio_block_on(fetch_metadata_hex(url))
pub fn fetch_metadata_hex_blocking(
url: &Uri,
version: MetadataVersion,
) -> Result<String, FetchMetadataError> {
tokio_block_on(fetch_metadata_hex(url, version))
}
// Block on some tokio runtime for sync contexts
@@ -32,26 +70,36 @@ fn tokio_block_on<T, Fut: std::future::Future<Output = T>>(fut: Fut) -> T {
}
/// Returns the metadata bytes from the provided URL.
pub async fn fetch_metadata_bytes(url: &Uri) -> Result<Vec<u8>, FetchMetadataError> {
let hex = fetch_metadata_hex(url).await?;
let bytes = hex::decode(hex.trim_start_matches("0x"))?;
Ok(bytes)
}
/// Returns the raw, 0x prefixed metadata hex from the provided URL.
pub async fn fetch_metadata_hex(url: &Uri) -> Result<String, FetchMetadataError> {
let hex_data = match url.scheme_str() {
Some("http") | Some("https") => fetch_metadata_http(url).await,
Some("ws") | Some("wss") => fetch_metadata_ws(url).await,
pub async fn fetch_metadata_bytes(
url: &Uri,
version: MetadataVersion,
) -> Result<Vec<u8>, FetchMetadataError> {
let bytes = match url.scheme_str() {
Some("http") | Some("https") => fetch_metadata_http(url, version).await,
Some("ws") | Some("wss") => fetch_metadata_ws(url, version).await,
invalid_scheme => {
let scheme = invalid_scheme.unwrap_or("no scheme");
Err(FetchMetadataError::InvalidScheme(scheme.to_owned()))
}
}?;
Ok(bytes)
}
/// Returns the raw, 0x prefixed metadata hex from the provided URL.
pub async fn fetch_metadata_hex(
url: &Uri,
version: MetadataVersion,
) -> Result<String, FetchMetadataError> {
let bytes = fetch_metadata_bytes(url, version).await?;
let hex_data = format!("0x{}", hex::encode(bytes));
Ok(hex_data)
}
async fn fetch_metadata_ws(url: &Uri) -> Result<String, FetchMetadataError> {
async fn fetch_metadata_ws(
url: &Uri,
version: MetadataVersion,
) -> Result<Vec<u8>, FetchMetadataError> {
let (sender, receiver) = WsTransportClientBuilder::default()
.build(url.to_string().parse::<Uri>().unwrap())
.await
@@ -62,13 +110,124 @@ async fn fetch_metadata_ws(url: &Uri) -> Result<String, FetchMetadataError> {
.max_notifs_per_subscription(4096)
.build_with_tokio(sender, receiver);
Ok(client.request("state_getMetadata", rpc_params![]).await?)
fetch_metadata(client, version).await
}
async fn fetch_metadata_http(url: &Uri) -> Result<String, FetchMetadataError> {
async fn fetch_metadata_http(
url: &Uri,
version: MetadataVersion,
) -> Result<Vec<u8>, FetchMetadataError> {
let client = HttpClientBuilder::default()
.request_timeout(Duration::from_secs(180))
.build(url.to_string())?;
Ok(client.request("state_getMetadata", rpc_params![]).await?)
fetch_metadata(client, version).await
}
/// The innermost call to fetch metadata:
async fn fetch_metadata(
client: impl ClientT,
version: MetadataVersion,
) -> Result<Vec<u8>, FetchMetadataError> {
const UNSTABLE_METADATA_VERSION: u32 = u32::MAX;
// Fetch metadata using the "new" state_call interface
async fn fetch_inner(
client: &impl ClientT,
version: MetadataVersion,
) -> Result<Vec<u8>, FetchMetadataError> {
// Look up supported versions:
let supported_versions: Vec<u32> = {
let res: String = client
.request(
"state_call",
rpc_params!["Metadata_metadata_versions", "0x"],
)
.await?;
let raw_bytes = hex::decode(res.trim_start_matches("0x"))?;
Decode::decode(&mut &raw_bytes[..])?
};
// Return the version the user wants if it's supported:
let version = match version {
MetadataVersion::Latest => *supported_versions
.iter()
.filter(|&&v| v != UNSTABLE_METADATA_VERSION)
.max()
.ok_or_else(|| {
FetchMetadataError::Other("No valid metadata versions returned".to_string())
})?,
MetadataVersion::Unstable => {
if supported_versions.contains(&UNSTABLE_METADATA_VERSION) {
UNSTABLE_METADATA_VERSION
} else {
return Err(FetchMetadataError::Other(
"The node does not have an unstable metadata version available".to_string(),
));
}
}
MetadataVersion::Version(version) => {
if supported_versions.contains(&version) {
version
} else {
return Err(FetchMetadataError::Other(format!(
"The node does not have version {version} available"
)));
}
}
};
let bytes = version.encode();
let version: String = format!("0x{}", hex::encode(&bytes));
// Fetch the metadata at that version:
let metadata_string: String = client
.request(
"state_call",
rpc_params!["Metadata_metadata_at_version", &version],
)
.await?;
// Decode the metadata.
let metadata_bytes = hex::decode(metadata_string.trim_start_matches("0x"))?;
let metadata: Option<frame_metadata::OpaqueMetadata> =
Decode::decode(&mut &metadata_bytes[..])?;
let Some(metadata) = metadata else {
return Err(FetchMetadataError::Other(format!(
"The node does not have version {version} available"
)));
};
Ok(metadata.0)
}
// Fetch metadata using the "old" state_call interface
async fn fetch_inner_legacy(
client: &impl ClientT,
version: MetadataVersion,
) -> Result<Vec<u8>, FetchMetadataError> {
if !matches!(
version,
MetadataVersion::Latest | MetadataVersion::Version(14)
) {
return Err(FetchMetadataError::Other(
"The node can only return version 14 metadata but you've asked for something else"
.to_string(),
));
}
// Fetch the metadata at that version:
let metadata_string: String = client
.request("state_call", rpc_params!["Metadata_metadata", "0x"])
.await?;
// Decode the metadata.
let metadata_bytes = hex::decode(metadata_string.trim_start_matches("0x"))?;
let metadata: frame_metadata::OpaqueMetadata = Decode::decode(&mut &metadata_bytes[..])?;
Ok(metadata.0)
}
// Fetch using the new interface, falling back to trying old one if there's an error.
match fetch_inner(&client, version).await {
Ok(s) => Ok(s),
Err(_) => fetch_inner_legacy(&client, version).await,
}
}
+1 -1
View File
@@ -11,5 +11,5 @@ pub use jsonrpsee::client_transport::ws::Uri;
pub use fetch_metadata::{
fetch_metadata_bytes, fetch_metadata_bytes_blocking, fetch_metadata_hex,
fetch_metadata_hex_blocking,
fetch_metadata_hex_blocking, MetadataVersion,
};
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! To run this example, a local polkadot node should be running. Example verified against polkadot v0.9.28-9ffe6e9e3da.
//!
//! E.g.
//! ```bash
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.28/polkadot" --output /usr/local/bin/polkadot --location
//! polkadot --dev --tmp
//! ```
use sp_keyring::AccountKeyring;
use subxt::dynamic::Value;
use subxt::{config::PolkadotConfig, OnlineClient};
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
pub mod polkadot {}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// Create a client to use:
let api = OnlineClient::<PolkadotConfig>::new().await?;
// In the first part of the example calls are made using the static generated code
// and as a result the returned values are strongly typed.
// Create a runtime API payload that calls into
// `Core_version` function.
let runtime_api_call = polkadot::apis().core().version();
// Submit the runtime API call.
let version = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await;
println!("Core_version: {:?}", version);
// Show the supported metadata versions of the node.
// Calls into `Metadata_metadata_versions` runtime function.
let runtime_api_call = polkadot::apis().metadata().metadata_versions();
// Submit the runtime API call.
let versions = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await?;
println!("Metadata_metadata_versions: {:?}", versions);
// Create a runtime API payload that calls into
// `AccountNonceApi_account_nonce` function.
let account = AccountKeyring::Alice.to_account_id().into();
let runtime_api_call = polkadot::apis().account_nonce_api().account_nonce(account);
// Submit the runtime API call.
let nonce = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await;
println!("AccountNonceApi_account_nonce for Alice: {:?}", nonce);
// Dynamic calls.
let runtime_api_call = subxt::dynamic::runtime_api_call(
"Metadata_metadata_versions",
Vec::<Value<()>>::new(),
None,
);
let versions = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await?;
println!(
" dynamic Metadata_metadata_versions: {:#?}",
versions.to_value()
);
Ok(())
}
+1 -1
View File
@@ -10,7 +10,7 @@ use frame_metadata::{v14::RuntimeMetadataV14, v15::RuntimeMetadataV15};
pub use retain::retain_metadata_pallets;
pub use validation::{
get_call_hash, get_constant_hash, get_metadata_hash, get_metadata_per_pallet_hash,
get_pallet_hash, get_storage_hash, NotFound,
get_pallet_hash, get_runtime_api_hash, get_runtime_trait_hash, get_storage_hash, NotFound,
};
/// Convert the metadata V14 to the latest metadata version.
+35 -1
View File
@@ -5,7 +5,7 @@
//! Utility functions to generate a subset of the metadata.
use frame_metadata::v15::{
ExtrinsicMetadata, PalletMetadata, RuntimeMetadataV15, StorageEntryType,
ExtrinsicMetadata, PalletMetadata, RuntimeApiMetadata, RuntimeMetadataV15, StorageEntryType,
};
use scale_info::{form::PortableForm, interner::UntrackedSymbol, TypeDef};
use std::{
@@ -105,6 +105,36 @@ fn update_extrinsic_types(
}
}
/// Collect all type IDs needed to represent the runtime APIs.
fn collect_runtime_api_types(
apis: &[RuntimeApiMetadata<PortableForm>],
type_ids: &mut HashSet<u32>,
) {
for api in apis {
for method in &api.methods {
for input in &method.inputs {
type_ids.insert(input.ty.id);
}
type_ids.insert(method.output.id);
}
}
}
/// Update all type IDs of the provided runtime APIs metadata using the new type IDs from the portable registry.
fn update_runtime_api_types(
apis: &mut [RuntimeApiMetadata<PortableForm>],
map_ids: &BTreeMap<u32, u32>,
) {
for api in apis {
for method in &mut api.methods {
for input in &mut method.inputs {
update_type(&mut input.ty, map_ids);
}
update_type(&mut method.output, map_ids);
}
}
}
/// Update the given type using the new type ID from the portable registry.
///
/// # Panics
@@ -191,6 +221,9 @@ where
// Keep the "runtime" type ID, since it's referenced in our metadata.
type_ids.insert(metadata.ty.id);
// Keep the runtime APIs types.
collect_runtime_api_types(&metadata.apis, &mut type_ids);
// Additionally, subxt depends on the `DispatchError` type existing; we use the same
// logic here that is used when building our `Metadata`.
let dispatch_error_ty = metadata
@@ -211,6 +244,7 @@ where
}
update_extrinsic_types(&mut metadata.extrinsic, &map_ids);
update_type(&mut metadata.ty, &map_ids);
update_runtime_api_types(&mut metadata.apis, &map_ids);
}
#[cfg(test)]
+124 -7
View File
@@ -5,7 +5,8 @@
//! Utility functions for metadata validation.
use frame_metadata::v15::{
ExtrinsicMetadata, PalletMetadata, RuntimeMetadataV15, StorageEntryMetadata, StorageEntryType,
ExtrinsicMetadata, PalletMetadata, RuntimeApiMetadata, RuntimeApiMethodMetadata,
RuntimeMetadataV15, StorageEntryMetadata, StorageEntryType,
};
use scale_info::{form::PortableForm, Field, PortableRegistry, TypeDef, Variant};
use std::collections::HashSet;
@@ -253,7 +254,7 @@ pub fn get_storage_hash(
.pallets
.iter()
.find(|p| p.name == pallet_name)
.ok_or(NotFound::Pallet)?;
.ok_or(NotFound::Root)?;
let storage = pallet.storage.as_ref().ok_or(NotFound::Item)?;
@@ -277,7 +278,7 @@ pub fn get_constant_hash(
.pallets
.iter()
.find(|p| p.name == pallet_name)
.ok_or(NotFound::Pallet)?;
.ok_or(NotFound::Root)?;
let constant = pallet
.constants
@@ -300,7 +301,7 @@ pub fn get_call_hash(
.pallets
.iter()
.find(|p| p.name == pallet_name)
.ok_or(NotFound::Pallet)?;
.ok_or(NotFound::Root)?;
let call_id = pallet.calls.as_ref().ok_or(NotFound::Item)?.ty.id;
@@ -321,6 +322,96 @@ pub fn get_call_hash(
Ok(hash)
}
fn get_runtime_method_hash(
metadata: &RuntimeMetadataV15,
trait_metadata: &RuntimeApiMetadata<PortableForm>,
method_metadata: &RuntimeApiMethodMetadata<PortableForm>,
visited_ids: &mut HashSet<u32>,
) -> [u8; 32] {
// The trait name is part of the runtime API call that is being
// generated for this method. Therefore the trait name is strongly
// connected to the method in the same way as a parameter is
// to the method.
let mut bytes = hash(trait_metadata.name.as_bytes());
bytes = xor(bytes, hash(method_metadata.name.as_bytes()));
for input in &method_metadata.inputs {
bytes = xor(bytes, hash(input.name.as_bytes()));
bytes = xor(
bytes,
get_type_hash(&metadata.types, input.ty.id, visited_ids),
);
}
bytes = xor(
bytes,
get_type_hash(&metadata.types, method_metadata.output.id, visited_ids),
);
bytes
}
/// Obtain the hash of a specific runtime trait.
pub fn get_runtime_trait_hash(
metadata: &RuntimeMetadataV15,
trait_metadata: &RuntimeApiMetadata<PortableForm>,
) -> [u8; 32] {
// Start out with any hash, the trait name is already part of the
// runtime method hash.
let mut bytes = hash(trait_metadata.name.as_bytes());
let mut visited_ids = HashSet::new();
let mut methods: Vec<_> = trait_metadata
.methods
.iter()
.map(|method_metadata| {
let bytes = get_runtime_method_hash(
metadata,
trait_metadata,
method_metadata,
&mut visited_ids,
);
(&*method_metadata.name, bytes)
})
.collect();
// Sort by method name to create a deterministic representation of the underlying metadata.
methods.sort_by_key(|&(name, _hash)| name);
// Note: Hash already takes into account the method name.
for (_, hash) in methods {
bytes = xor(bytes, hash);
}
bytes
}
/// Obtain the hash of a specific runtime API function, or an error if it's not found.
pub fn get_runtime_api_hash(
metadata: &RuntimeMetadataV15,
trait_name: &str,
method_name: &str,
) -> Result<[u8; 32], NotFound> {
let trait_metadata = metadata
.apis
.iter()
.find(|m| m.name == trait_name)
.ok_or(NotFound::Root)?;
let method_metadata = trait_metadata
.methods
.iter()
.find(|m| m.name == method_name)
.ok_or(NotFound::Item)?;
Ok(get_runtime_method_hash(
metadata,
trait_metadata,
method_metadata,
&mut HashSet::new(),
))
}
/// Obtain the hash representation of a `frame_metadata::v15::PalletMetadata`.
pub fn get_pallet_hash(
registry: &PortableRegistry,
@@ -404,6 +495,19 @@ pub fn get_metadata_hash(metadata: &RuntimeMetadataV15) -> [u8; 32] {
&mut visited_ids,
));
let mut apis: Vec<_> = metadata
.apis
.iter()
.map(|api| (&*api.name, get_runtime_trait_hash(metadata, api)))
.collect();
// Sort the runtime APIs by trait name to provide a deterministic output.
apis.sort_by_key(|&(name, _hash)| name);
for (_, hash) in apis.iter() {
bytes.extend(hash)
}
hash(&bytes)
}
@@ -449,11 +553,24 @@ pub fn get_metadata_per_pallet_hash<T: AsRef<str>>(
hash(&bytes)
}
/// An error returned if we attempt to get the hash for a specific call, constant
/// or storage item that doesn't exist.
/// An error returned if we attempt to get the hash for a specific call, constant,
/// storage or runtime API function does not exist.
///
/// The location of the specific item (call, constant, storage or runtime API function)
/// is stored with two indirections:
/// - Root
/// The root location of the item. For calls, constants, storage this represents the
/// pallet name. While for runtime API function this represents the trait name.
/// - Item
/// The actual item. For calls, constants, storage this represents the actual name.
/// While for runtime API functions this represents the method name.
#[derive(Clone, Debug)]
pub enum NotFound {
Pallet,
/// The root location of the item cannot be found.
/// - pallet name: for calls, constants, storage
/// - trait name: for runtime API functions
Root,
/// The actual item name cannot be found.
Item,
}
+6
View File
@@ -34,6 +34,12 @@ integration-tests = []
jsonrpsee-ws = ["jsonrpsee/async-client", "jsonrpsee/client-ws-transport"]
jsonrpsee-web = ["jsonrpsee/async-wasm-client", "jsonrpsee/client-web-transport"]
# Activate this to fetch and utilize the latest unstabl metadata from a node.
# The unstable metadata is subject to breaking changes and the subxt might
# fail to decode the metadata properly. Use this to experiment with the
# latest features exposed by the metadata.
unstable-metadata = []
[dependencies]
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
scale-info = { workspace = true }
+14 -7
View File
@@ -17,9 +17,7 @@ use crate::{
tx::TxClient,
Config, Metadata,
};
use codec::Compact;
use derivative::Derivative;
use frame_metadata::RuntimeMetadataPrefixed;
use futures::future;
use parking_lot::RwLock;
use std::sync::Arc;
@@ -136,10 +134,19 @@ impl<T: Config> OnlineClient<T> {
/// Fetch the metadata from substrate using the runtime API.
async fn fetch_metadata(rpc: &Rpc<T>) -> Result<Metadata, Error> {
let (_, meta) = rpc
.state_call::<(Compact<u32>, RuntimeMetadataPrefixed)>("Metadata_metadata", None, None)
.await?;
Ok(meta.try_into()?)
#[cfg(feature = "unstable-metadata")]
{
// Try to fetch the latest unstable metadata, if that fails fall back to
// fetching the latest stable metadata.
const V15_METADATA_VERSION: u32 = u32::MAX;
match rpc.metadata_at_version(V15_METADATA_VERSION).await {
Ok(bytes) => Ok(bytes),
Err(_) => rpc.metadata().await,
}
}
#[cfg(not(feature = "unstable-metadata"))]
rpc.metadata().await
}
/// Create an object which can be used to keep the runtime up to date
@@ -383,7 +390,7 @@ impl<T: Config> RuntimeUpdaterStream<T> {
Err(err) => return Some(Err(err)),
};
let metadata = match self.client.rpc().metadata(None).await {
let metadata = match self.client.rpc().metadata().await {
Ok(metadata) => metadata,
Err(err) => return Some(Err(err)),
};
+3
View File
@@ -28,6 +28,9 @@ pub use crate::constants::dynamic as constant;
// Lookup storage values dynamically.
pub use crate::storage::{dynamic as storage, dynamic_root as storage_root};
// Execute runtime API function call dynamically.
pub use crate::runtime_api::dynamic as runtime_api_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.
+1 -1
View File
@@ -76,7 +76,7 @@ impl<T: Config> Events<T> {
/// .await?
/// .expect("didn't pass a block number; qed");
/// // Fetch the metadata of the given block.
/// let metadata = client.rpc().metadata(Some(block_hash)).await?;
/// let metadata = client.rpc().metadata_legacy(Some(block_hash)).await?;
/// // Fetch the events from the client.
/// let events = Events::new_from_client(metadata, block_hash, client);
/// # Ok(())
+12 -13
View File
@@ -5,23 +5,23 @@
use parking_lot::RwLock;
use std::{borrow::Cow, collections::HashMap};
/// A cache with the simple goal of storing 32 byte hashes against pallet+item keys
/// A cache with the simple goal of storing 32 byte hashes against root+item keys
#[derive(Default, Debug)]
pub struct HashCache {
inner: RwLock<HashMap<PalletItemKey<'static>, [u8; 32]>>,
inner: RwLock<HashMap<RootItemKey<'static>, [u8; 32]>>,
}
impl HashCache {
/// get a hash out of the cache by its pallet and item key. If the item doesn't exist,
/// get a hash out of the cache by its root and item key. If the item doesn't exist,
/// run the function provided to obtain a hash to insert (or bail with some error on failure).
pub fn get_or_insert<F, E>(&self, pallet: &str, item: &str, f: F) -> Result<[u8; 32], E>
pub fn get_or_insert<F, E>(&self, root: &str, item: &str, f: F) -> Result<[u8; 32], E>
where
F: FnOnce() -> Result<[u8; 32], E>,
{
let maybe_hash = self
.inner
.read()
.get(&PalletItemKey::new(pallet, item))
.get(&RootItemKey::new(root, item))
.copied();
if let Some(hash) = maybe_hash {
@@ -29,10 +29,9 @@ impl HashCache {
}
let hash = f()?;
self.inner.write().insert(
PalletItemKey::new(pallet.to_string(), item.to_string()),
hash,
);
self.inner
.write()
.insert(RootItemKey::new(root.to_string(), item.to_string()), hash);
Ok(hash)
}
@@ -41,14 +40,14 @@ impl HashCache {
/// This exists so that we can look items up in the cache using &strs, without having to allocate
/// Strings first (as you'd have to do to construct something like an `&(String,String)` key).
#[derive(Debug, PartialEq, Eq, Hash)]
struct PalletItemKey<'a> {
struct RootItemKey<'a> {
pallet: Cow<'a, str>,
item: Cow<'a, str>,
}
impl<'a> PalletItemKey<'a> {
impl<'a> RootItemKey<'a> {
fn new(pallet: impl Into<Cow<'a, str>>, item: impl Into<Cow<'a, str>>) -> Self {
PalletItemKey {
RootItemKey {
pallet: pallet.into(),
item: item.into(),
}
@@ -75,7 +74,7 @@ mod tests {
cache
.inner
.read()
.get(&PalletItemKey::new(pallet, item))
.get(&RootItemKey::new(pallet, item))
.unwrap(),
&value.unwrap()
);
+116 -3
View File
@@ -30,6 +30,9 @@ pub enum MetadataError {
/// Event is not in metadata.
#[error("Pallet {0}, Error {0} not found")]
ErrorNotFound(u8, u8),
/// Runtime function is not in metadata.
#[error("Runtime function not found")]
RuntimeFnNotFound,
/// Storage is not in metadata.
#[error("Storage not found")]
StorageNotFound,
@@ -57,6 +60,9 @@ pub enum MetadataError {
/// Runtime storage metadata is incompatible with the static one.
#[error("Pallet {0} Storage {0} has incompatible metadata")]
IncompatibleStorageMetadata(String, String),
/// Runtime API metadata is incompatible with the static one.
#[error("Runtime API Trait {0} Method {0} has incompatible metadata")]
IncompatibleRuntimeApiMetadata(String, String),
/// Runtime metadata is not fully compatible with the static one.
#[error("Node metadata is not fully compatible")]
IncompatibleMetadata,
@@ -79,6 +85,9 @@ struct MetadataInner {
// an extrinsic fails.
dispatch_error_ty: Option<u32>,
// Runtime API metadata
runtime_apis: HashMap<String, RuntimeFnMetadata>,
// The hashes uniquely identify parts of the metadata; different
// hashes mean some type difference exists between static and runtime
// versions. We cache them here to avoid recalculating:
@@ -86,6 +95,7 @@ struct MetadataInner {
cached_call_hashes: HashCache,
cached_constant_hashes: HashCache,
cached_storage_hashes: HashCache,
cached_runtime_hashes: HashCache,
}
/// A representation of the runtime metadata received from a node.
@@ -95,6 +105,14 @@ pub struct Metadata {
}
impl Metadata {
/// Returns a reference to [`RuntimeFnMetadata`].
pub fn runtime_fn(&self, name: &str) -> Result<&RuntimeFnMetadata, MetadataError> {
self.inner
.runtime_apis
.get(name)
.ok_or(MetadataError::RuntimeFnNotFound)
}
/// Returns a reference to [`PalletMetadata`].
pub fn pallet(&self, name: &str) -> Result<&PalletMetadata, MetadataError> {
self.inner
@@ -158,7 +176,7 @@ impl Metadata {
.get_or_insert(pallet, storage, || {
subxt_metadata::get_storage_hash(&self.inner.metadata, pallet, storage).map_err(
|e| match e {
subxt_metadata::NotFound::Pallet => MetadataError::PalletNotFound,
subxt_metadata::NotFound::Root => MetadataError::PalletNotFound,
subxt_metadata::NotFound::Item => MetadataError::StorageNotFound,
},
)
@@ -172,7 +190,7 @@ impl Metadata {
.get_or_insert(pallet, constant, || {
subxt_metadata::get_constant_hash(&self.inner.metadata, pallet, constant).map_err(
|e| match e {
subxt_metadata::NotFound::Pallet => MetadataError::PalletNotFound,
subxt_metadata::NotFound::Root => MetadataError::PalletNotFound,
subxt_metadata::NotFound::Item => MetadataError::ConstantNotFound,
},
)
@@ -186,13 +204,27 @@ impl Metadata {
.get_or_insert(pallet, function, || {
subxt_metadata::get_call_hash(&self.inner.metadata, pallet, function).map_err(|e| {
match e {
subxt_metadata::NotFound::Pallet => MetadataError::PalletNotFound,
subxt_metadata::NotFound::Root => MetadataError::PalletNotFound,
subxt_metadata::NotFound::Item => MetadataError::CallNotFound,
}
})
})
}
/// Obtain the unique hash for a runtime API function.
pub fn runtime_api_hash(
&self,
trait_name: &str,
method_name: &str,
) -> Result<[u8; 32], MetadataError> {
self.inner
.cached_runtime_hashes
.get_or_insert(trait_name, method_name, || {
subxt_metadata::get_runtime_api_hash(&self.inner.metadata, trait_name, method_name)
.map_err(|_| MetadataError::RuntimeFnNotFound)
})
}
/// Obtain the unique hash for this metadata.
pub fn metadata_hash<T: AsRef<str>>(&self, pallets: &[T]) -> [u8; 32] {
if let Some(hash) = *self.inner.cached_metadata_hash.read() {
@@ -206,6 +238,42 @@ impl Metadata {
}
}
/// Metadata for a specific runtime API function.
#[derive(Clone, Debug)]
pub struct RuntimeFnMetadata {
/// The trait name of the runtime function.
trait_name: String,
/// The method name of the runtime function.
method_name: String,
/// The parameter name and type IDs interpreted as `scale_info::Field`
/// for ease of decoding.
fields: Vec<scale_info::Field<scale_info::form::PortableForm>>,
/// The type ID of the return type.
return_id: u32,
}
impl RuntimeFnMetadata {
/// Get the parameters as fields.
pub fn fields(&self) -> &[scale_info::Field<scale_info::form::PortableForm>] {
&self.fields
}
/// Return the trait name of the runtime function.
pub fn trait_name(&self) -> &str {
&self.trait_name
}
/// Return the method name of the runtime function.
pub fn method_name(&self) -> &str {
&self.method_name
}
/// Get the type ID of the return type.
pub fn return_id(&self) -> u32 {
self.return_id
}
}
/// Metadata for a specific pallet.
#[derive(Clone, Debug)]
pub struct PalletMetadata {
@@ -376,6 +444,49 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
_ => return Err(InvalidMetadataError::InvalidVersion),
};
let runtime_apis: HashMap<String, RuntimeFnMetadata> = metadata
.apis
.iter()
.flat_map(|trait_metadata| {
let trait_name = &trait_metadata.name;
trait_metadata
.methods
.iter()
.map(|method_metadata| {
// Function named used by substrate to identify the runtime call.
let fn_name = format!("{}_{}", trait_name, method_metadata.name);
// Parameters mapped as `scale_info::Field` to allow dynamic decoding.
let fields: Vec<_> = method_metadata
.inputs
.iter()
.map(|input| {
let name = input.name.clone();
let ty = input.ty.id;
scale_info::Field {
name: Some(name),
ty: ty.into(),
type_name: None,
docs: Default::default(),
}
})
.collect();
let return_id = method_metadata.output.id;
let metadata = RuntimeFnMetadata {
fields,
return_id,
trait_name: trait_name.clone(),
method_name: method_metadata.name.clone(),
};
(fn_name, metadata)
})
.collect::<Vec<_>>()
})
.collect();
let get_type_def_variant = |type_id: u32| {
let ty = metadata
.types
@@ -492,10 +603,12 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
events,
errors,
dispatch_error_ty,
runtime_apis,
cached_metadata_hash: Default::default(),
cached_call_hashes: Default::default(),
cached_constant_hashes: Default::default(),
cached_storage_hashes: Default::default(),
cached_runtime_hashes: Default::default(),
}),
})
}
+48 -5
View File
@@ -143,8 +143,8 @@ impl<T: Config> Rpc<T> {
genesis_hash.ok_or_else(|| "Genesis hash not found".into())
}
/// Fetch the metadata
pub async fn metadata(&self, at: Option<T::Hash>) -> Result<Metadata, Error> {
/// Fetch the metadata via the legacy `state_getMetadata` RPC method.
pub async fn metadata_legacy(&self, at: Option<T::Hash>) -> Result<Metadata, Error> {
let bytes: types::Bytes = self
.client
.request("state_getMetadata", rpc_params![at])
@@ -347,13 +347,13 @@ impl<T: Config> Rpc<T> {
Ok(xt_hash)
}
/// Execute a runtime API call.
pub async fn state_call<Res: Decode>(
/// Execute a runtime API call via `state_call` RPC method.
pub async fn state_call_raw(
&self,
function: &str,
call_parameters: Option<&[u8]>,
at: Option<T::Hash>,
) -> Result<Res, Error> {
) -> Result<types::Bytes, Error> {
let call_parameters = call_parameters.unwrap_or_default();
let bytes: types::Bytes = self
.client
@@ -362,11 +362,54 @@ impl<T: Config> Rpc<T> {
rpc_params![function, to_hex(call_parameters), at],
)
.await?;
Ok(bytes)
}
/// Execute a runtime API call and decode the result.
pub async fn state_call<Res: Decode>(
&self,
function: &str,
call_parameters: Option<&[u8]>,
at: Option<T::Hash>,
) -> Result<Res, Error> {
let bytes = self.state_call_raw(function, call_parameters, at).await?;
let cursor = &mut &bytes[..];
let res: Res = Decode::decode(cursor)?;
Ok(res)
}
/// Execute runtime API call and return the specified runtime metadata version.
pub async fn metadata_at_version(&self, version: u32) -> Result<Metadata, Error> {
let param = version.encode();
let opaque: Option<frame_metadata::OpaqueMetadata> = self
.state_call("Metadata_metadata_at_version", Some(&param), None)
.await?;
let bytes = opaque.ok_or(Error::Other("Metadata version not found".into()))?;
let meta: RuntimeMetadataPrefixed = Decode::decode(&mut &bytes.0[..])?;
let metadata: Metadata = meta.try_into()?;
Ok(metadata)
}
/// Execute a runtime API call into `Metadata_metadata` method
/// to fetch the latest available metadata.
///
/// # Note
///
/// This returns the same output as [`Self::metadata`], but calls directly
/// into the runtime.
pub async fn metadata(&self) -> Result<Metadata, Error> {
let bytes: frame_metadata::OpaqueMetadata =
self.state_call("Metadata_metadata", None, None).await?;
let meta: RuntimeMetadataPrefixed = Decode::decode(&mut &bytes.0[..])?;
let metadata: Metadata = meta.try_into()?;
Ok(metadata)
}
/// Create and submit an extrinsic and return a subscription to the events triggered.
pub async fn watch_extrinsic<X: Encode>(
&self,
+2
View File
@@ -5,7 +5,9 @@
//! Types associated with executing runtime API calls.
mod runtime_client;
mod runtime_payload;
mod runtime_types;
pub use runtime_client::RuntimeApiClient;
pub use runtime_payload::{dynamic, DynamicRuntimeApiPayload, Payload, RuntimeApiPayload};
pub use runtime_types::RuntimeApi;
+162
View File
@@ -0,0 +1,162 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use core::marker::PhantomData;
use scale_encode::EncodeAsFields;
use scale_value::Composite;
use std::borrow::Cow;
use crate::dynamic::DecodedValueThunk;
use crate::{metadata::DecodeWithMetadata, Error, Metadata};
/// This represents a runtime API payload that can call into the runtime of node.
///
/// # Components
///
/// - associated return type
///
/// Resulting bytes of the call are interpreted into this type.
///
/// - runtime function name
///
/// The function name of the runtime API call. This is obtained by concatenating
/// the runtime trait name with the trait's method.
///
/// For example, the substrate runtime trait [Metadata](https://github.com/paritytech/substrate/blob/cb954820a8d8d765ce75021e244223a3b4d5722d/primitives/api/src/lib.rs#L745)
/// contains the `metadata_at_version` function. The corresponding runtime function
/// is `Metadata_metadata_at_version`.
///
/// - encoded arguments
///
/// Each argument of the runtime function must be scale-encoded.
pub trait RuntimeApiPayload {
/// 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 runtime API function name.
fn fn_name(&self) -> &str;
/// 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 [`RuntimeApiPayload::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 runtime API 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(Clone, Debug)]
pub struct Payload<ArgsData, ReturnTy> {
fn_name: Cow<'static, str>,
args_data: ArgsData,
validation_hash: Option<[u8; 32]>,
_marker: PhantomData<ReturnTy>,
}
impl<ArgsData: EncodeAsFields, ReturnTy: DecodeWithMetadata> RuntimeApiPayload
for Payload<ArgsData, ReturnTy>
{
type ReturnType = ReturnTy;
fn fn_name(&self) -> &str {
&self.fn_name
}
fn encode_args_to(&self, metadata: &Metadata, out: &mut Vec<u8>) -> Result<(), Error> {
let fn_metadata = metadata.runtime_fn(&self.fn_name)?;
self.args_data
.encode_as_fields_to(fn_metadata.fields(), metadata.types(), out)?;
Ok(())
}
fn validation_hash(&self) -> Option<[u8; 32]> {
self.validation_hash
}
}
/// A dynamic runtime API payload.
pub type DynamicRuntimeApiPayload = Payload<Composite<()>, DecodedValueThunk>;
impl<ReturnTy, ArgsData> Payload<ArgsData, ReturnTy> {
/// Create a new [`Payload`].
pub fn new(
fn_name: impl Into<String>,
args_data: ArgsData,
validation_hash: Option<[u8; 32]>,
) -> Self {
Payload {
fn_name: Cow::Owned(fn_name.into()),
args_data,
validation_hash,
_marker: PhantomData,
}
}
/// Create a new static [`Payload`] using static function name
/// and scale-encoded argument data.
///
/// This is only expected to be used from codegen.
#[doc(hidden)]
pub fn new_static(
fn_name: &'static str,
args_data: ArgsData,
hash: [u8; 32],
) -> Payload<ArgsData, ReturnTy> {
Payload {
fn_name: Cow::Borrowed(fn_name),
args_data,
validation_hash: Some(hash),
_marker: std::marker::PhantomData,
}
}
/// Do not validate this call prior to submitting it.
pub fn unvalidated(self) -> Self {
Self {
validation_hash: None,
..self
}
}
/// Returns the function name.
pub fn fn_name(&self) -> &str {
&self.fn_name
}
/// Returns the arguments data.
pub fn args_data(&self) -> &ArgsData {
&self.args_data
}
}
/// Create a new [`DynamicRuntimeApiPayload`].
pub fn dynamic(
fn_name: impl Into<String>,
args_data: impl Into<Composite<()>>,
hash: Option<[u8; 32]>,
) -> DynamicRuntimeApiPayload {
DynamicRuntimeApiPayload {
fn_name: Cow::Owned(fn_name.into()),
args_data: args_data.into(),
validation_hash: hash,
_marker: std::marker::PhantomData,
}
}
+56 -1
View File
@@ -2,11 +2,13 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{client::OnlineClientT, error::Error, Config};
use crate::{client::OnlineClientT, error::Error, metadata::DecodeWithMetadata, Config};
use codec::Decode;
use derivative::Derivative;
use std::{future::Future, marker::PhantomData};
use super::RuntimeApiPayload;
/// Execute runtime API calls.
#[derive(Derivative)]
#[derivative(Clone(bound = "Client: Clone"))]
@@ -50,4 +52,57 @@ where
Ok(data)
}
}
/// Execute a runtime API call.
pub fn call<Call: RuntimeApiPayload>(
&self,
payload: Call,
) -> impl Future<Output = Result<Call::ReturnType, Error>> {
let client = self.client.clone();
let block_hash = self.block_hash;
// Ensure that the returned future doesn't have a lifetime tied to api.runtime_api(),
// which is a temporary thing we'll be throwing away quickly:
async move {
let metadata = client.metadata();
let function = payload.fn_name();
// Check if the function is present in the runtime metadata.
let fn_metadata = metadata.runtime_fn(function)?;
// Return type ID used for dynamic decoding.
let return_id = fn_metadata.return_id();
// Validate the runtime API payload hash against the compile hash from codegen.
if let Some(static_hash) = payload.validation_hash() {
let runtime_hash = metadata
.runtime_api_hash(fn_metadata.trait_name(), fn_metadata.method_name())?;
if static_hash != runtime_hash {
return Err(
crate::metadata::MetadataError::IncompatibleRuntimeApiMetadata(
fn_metadata.trait_name().into(),
fn_metadata.method_name().into(),
)
.into(),
);
}
}
// Encode the arguments of the runtime call.
// For static payloads (codegen) this is pass-through, bytes are not altered.
// For dynamic payloads this relies on `scale_value::encode_as_fields_to`.
let params = payload.encode_args(&metadata)?;
let bytes = client
.rpc()
.state_call_raw(function, Some(params.as_slice()), Some(block_hash))
.await?;
let value = <Call::ReturnType as DecodeWithMetadata>::decode_with_metadata(
&mut &bytes[..],
return_id,
&metadata,
)?;
Ok(value)
}
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ sp-core = { workspace = true }
sp-runtime = { workspace = true }
sp-keyring = { workspace = true }
syn = { workspace = true }
subxt = { workspace = true }
subxt = { workspace = true, features = ["unstable-metadata"] }
subxt-codegen = { workspace = true }
subxt-metadata = { workspace = true }
test-runtime = { workspace = true }
+1 -1
View File
@@ -113,7 +113,7 @@ async fn runtime_api_call() -> Result<(), subxt::Error> {
};
// Compare the runtime API call against the `state_getMetadata`.
let metadata = api.rpc().metadata(None).await?;
let metadata = api.rpc().metadata_legacy(None).await?;
let metadata = metadata.runtime_metadata();
assert_eq!(&metadata_call, metadata);
Ok(())
+1 -1
View File
@@ -438,7 +438,7 @@ async fn rpc_state_call() {
_ => panic!("Metadata V14 or V15 unavailable"),
};
// Compare the runtime API call against the `state_getMetadata`.
let metadata = api.rpc().metadata(None).await.unwrap();
let metadata = api.rpc().metadata_legacy(None).await.unwrap();
let metadata = metadata.runtime_metadata();
assert_eq!(&metadata_call, metadata);
}
@@ -40,6 +40,14 @@ fn metadata_docs() -> Vec<String> {
// Note: Extrinsics do not have associated documentation, but is implied by
// associated Type.
// Inspect the runtime API types and collect the documentation.
for api in metadata.apis {
docs.extend(api.docs);
for method in api.methods {
docs.extend(method.docs);
}
}
docs
}
File diff suppressed because one or more lines are too long
+2
View File
@@ -18,6 +18,8 @@ mod frame;
#[cfg(test)]
mod metadata;
#[cfg(test)]
mod runtime_api;
#[cfg(test)]
mod storage;
#[cfg(test)]
@@ -0,0 +1,49 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{node_runtime, pair_signer, test_context};
use sp_keyring::AccountKeyring;
use subxt::utils::AccountId32;
#[tokio::test]
async fn account_nonce() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let signer = pair_signer(AccountKeyring::Alice.pair());
let alice: AccountId32 = AccountKeyring::Alice.to_account_id().into();
// Check Alice nonce is starting from 0.
let runtime_api_call = node_runtime::apis()
.account_nonce_api()
.account_nonce(alice.clone());
let nonce = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await?;
assert_eq!(nonce, 0);
// Do some transaction to bump the Alice nonce to 1:
let remark_tx = node_runtime::tx().system().remark(vec![1, 2, 3, 4, 5]);
api.tx()
.sign_and_submit_then_watch_default(&remark_tx, &signer)
.await?
.wait_for_finalized_success()
.await?;
let runtime_api_call = node_runtime::apis()
.account_nonce_api()
.account_nonce(alice);
let nonce = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await?;
assert_eq!(nonce, 1);
Ok(())
}
+2
View File
@@ -14,3 +14,5 @@ serde = { workspace = true }
tokio = { workspace = true }
which = { workspace = true }
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport"] }
hex = { workspace = true }
codec = { workspace = true }
+15 -5
View File
@@ -2,6 +2,7 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use codec::{Decode, Encode};
use std::{env, fs, path::Path};
use substrate_runner::{Error as SubstrateNodeError, SubstrateNode};
@@ -36,22 +37,31 @@ async fn run() {
// 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).
#[derive(serde::Deserialize)]
pub struct Bytes(#[serde(with = "impl_serde::serialize")] pub Vec<u8>);
let metadata_bytes: Bytes = {
const V15_METADATA_VERSION: u32 = u32::MAX;
let bytes = V15_METADATA_VERSION.encode();
let version: String = format!("0x{}", hex::encode(&bytes));
let raw: String = {
use client::ClientT;
client::build(&format!("ws://localhost:{port}"))
.await
.unwrap_or_else(|e| panic!("Failed to connect to node: {e}"))
.request("state_getMetadata", client::rpc_params![])
.request(
"state_call",
client::rpc_params!["Metadata_metadata_at_version", &version],
)
.await
.unwrap_or_else(|e| panic!("Failed to obtain metadata from node: {e}"))
};
let raw_bytes = hex::decode(raw.trim_start_matches("0x"))
.unwrap_or_else(|e| panic!("Failed to hex-decode metadata: {e}"));
let bytes: Option<Vec<u8>> = Decode::decode(&mut &raw_bytes[..])
.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("metadata.scale");
fs::write(&metadata_path, metadata_bytes.0).expect("Couldn't write metadata output");
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`,