mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-15 17:21:08 +00:00
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:
Generated
+2
@@ -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.
@@ -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
@@ -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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
#(
|
||||
|
||||
@@ -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 )*
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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
@@ -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(¶m), 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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user