diff --git a/core/src/constants/mod.rs b/core/src/constants/mod.rs index a9195d9e14..607877c001 100644 --- a/core/src/constants/mod.rs +++ b/core/src/constants/mod.rs @@ -37,7 +37,7 @@ pub fn validate_constant( Ok(()) } -pub fn access_constant( +pub fn get_constant( metadata: &Metadata, address: &Address, ) -> Result { diff --git a/core/src/custom_values/custom_value_address.rs b/core/src/custom_values/custom_value_address.rs new file mode 100644 index 0000000000..997d6c8e42 --- /dev/null +++ b/core/src/custom_values/custom_value_address.rs @@ -0,0 +1,79 @@ +use derivative::Derivative; + +use crate::dynamic::DecodedValueThunk; +use crate::metadata::DecodeWithMetadata; + +/// This represents the address of a custom value in in the metadata. +/// Anything, that implements the [CustomValueAddress] trait can be used, to fetch +/// custom values from the metadata. +/// The trait is implemented by [str] for dynamic loopup and [StaticAddress] for static queries. +pub trait CustomValueAddress { + /// The type of the custom value. + type Target: DecodeWithMetadata; + /// Should be set to `Yes` for Dynamic values and static values that have a valid type. + /// Should be `()` for custom values, that have an invalid type id. + type IsDecodable; + + /// the name (key) by which the custom value can be accessed in the metadata. + fn name(&self) -> &str; + + /// An optional hash which, if present, can be checked against node metadata. + fn validation_hash(&self) -> Option<[u8; 32]> { + None + } +} + +impl CustomValueAddress for str { + type Target = DecodedValueThunk; + type IsDecodable = Yes; + + fn name(&self) -> &str { + self + } +} + +/// Used to signal whether a [`CustomValueAddress`] can be decoded. +pub struct Yes; + +/// A static address to a custom value. +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""))] +pub struct StaticAddress { + name: &'static str, + hash: Option<[u8; 32]>, + phantom: core::marker::PhantomData<(ReturnTy, IsDecodable)>, +} + +impl StaticAddress { + #[doc(hidden)] + /// Creates a new StaticAddress. + pub fn new_static(name: &'static str, hash: [u8; 32]) -> StaticAddress { + StaticAddress:: { + name, + hash: Some(hash), + phantom: core::marker::PhantomData, + } + } + + /// Do not validate this custom value prior to accessing it. + pub fn unvalidated(self) -> Self { + Self { + name: self.name, + hash: None, + phantom: self.phantom, + } + } +} + +impl CustomValueAddress for StaticAddress { + type Target = R; + type IsDecodable = Y; + + fn name(&self) -> &str { + self.name + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + self.hash + } +} diff --git a/core/src/custom_values/mod.rs b/core/src/custom_values/mod.rs new file mode 100644 index 0000000000..d0d418bc0e --- /dev/null +++ b/core/src/custom_values/mod.rs @@ -0,0 +1,150 @@ +// 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. + +//! Types associated with accessing custom types + +mod custom_value_address; + +pub use custom_value_address::{CustomValueAddress, StaticAddress, Yes}; + +use crate::{ + metadata::{DecodeWithMetadata, MetadatExt}, + Error, Metadata, MetadataError, +}; +use alloc::vec::Vec; + +/// Run the validation logic against some custom value address you'd like to access. Returns `Ok(())` +/// if the address is valid (or if it's not possible to check since the address has no validation hash). +/// Returns an error if the address was not valid (wrong name, type or raw bytes) +pub fn validate_custom_value( + metadata: &Metadata, + address: &Address, +) -> Result<(), Error> { + if let Some(actual_hash) = address.validation_hash() { + let custom = metadata.custom(); + let custom_value = custom + .get(address.name()) + .ok_or_else(|| MetadataError::CustomValueNameNotFound(address.name().into()))?; + let expected_hash = custom_value.hash(); + if actual_hash != expected_hash { + return Err(MetadataError::IncompatibleCodegen.into()); + } + } + if metadata.custom().get(address.name()).is_none() { + return Err(MetadataError::IncompatibleCodegen.into()); + } + Ok(()) +} + +/// Access a custom value by the address it is registered under. This can be just a [str] to get back a dynamic value, +/// or a static address from the generated static interface to get a value of a static type returned. +pub fn get_custom_value + ?Sized>( + metadata: &Metadata, + address: &Address, +) -> Result { + // 1. Validate custom value shape if hash given: + validate_custom_value(metadata, address)?; + + // 2. Attempt to decode custom value: + let custom_value = metadata.custom_value_by_name_err(address.name())?; + let value = ::decode_with_metadata( + &mut custom_value.bytes(), + custom_value.type_id(), + &metadata, + )?; + Ok(value) +} + +/// Access the bytes of a custom value by the address it is registered under. +pub fn get_custom_value_bytes( + metadata: &Metadata, + address: &Address, +) -> Result, Error> { + // 1. Validate custom value shape if hash given: + validate_custom_value(metadata, address)?; + + // 2. Return the underlying bytes: + let custom_value = metadata.custom_value_by_name_err(address.name())?; + Ok(custom_value.bytes().to_vec()) +} + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + use codec::Encode; + use scale_decode::DecodeAsType; + use scale_info::form::PortableForm; + use scale_info::TypeInfo; + + use crate::custom_values::get_custom_value; + use crate::Metadata; + + #[derive(Debug, Clone, PartialEq, Eq, Encode, TypeInfo, DecodeAsType)] + pub struct Person { + age: u16, + name: String, + } + + fn mock_metadata() -> Metadata { + let person_ty = scale_info::MetaType::new::(); + let unit = scale_info::MetaType::new::<()>(); + let mut types = scale_info::Registry::new(); + let person_ty_id = types.register_type(&person_ty); + let unit_id = types.register_type(&unit); + let types: scale_info::PortableRegistry = types.into(); + + let person = Person { + age: 42, + name: "Neo".into(), + }; + + let person_value_metadata: frame_metadata::v15::CustomValueMetadata = + frame_metadata::v15::CustomValueMetadata { + ty: person_ty_id, + value: person.encode(), + }; + + let frame_metadata = frame_metadata::v15::RuntimeMetadataV15 { + types, + pallets: vec![], + extrinsic: frame_metadata::v15::ExtrinsicMetadata { + version: 0, + address_ty: unit_id, + call_ty: unit_id, + signature_ty: unit_id, + extra_ty: unit_id, + signed_extensions: vec![], + }, + ty: unit_id, + apis: vec![], + outer_enums: frame_metadata::v15::OuterEnums { + call_enum_ty: unit_id, + event_enum_ty: unit_id, + error_enum_ty: unit_id, + }, + custom: frame_metadata::v15::CustomMetadata { + map: BTreeMap::from_iter([("Mr. Robot".to_string(), person_value_metadata)]), + }, + }; + + let metadata: subxt_metadata::Metadata = frame_metadata.try_into().unwrap(); + Metadata::new(metadata) + } + + #[test] + fn test_decoding() { + let metadata = mock_metadata(); + + assert!(get_custom_value(&metadata, "Invalid Address").is_err()); + let person_decoded_value_thunk = get_custom_value(&metadata, "Mr. Robot").unwrap(); + let person: Person = person_decoded_value_thunk.as_type().unwrap(); + assert_eq!( + person, + Person { + age: 42, + name: "Neo".into() + } + ) + } +} diff --git a/core/src/dynamic.rs b/core/src/dynamic.rs index bfab442e65..fdf2d7b58f 100644 --- a/core/src/dynamic.rs +++ b/core/src/dynamic.rs @@ -16,17 +16,17 @@ pub use scale_value::{At, Value}; /// for dynamic requests. pub type DecodedValue = scale_value::Value; -// // Submit dynamic transactions. +/// Submit dynamic transactions. pub use crate::tx::dynamic as tx; -// // Lookup constants dynamically. +/// Lookup constants dynamically. pub use crate::constants::dynamic as constant; -// // Lookup storage values dynamically. +/// Lookup storage values dynamically. pub use crate::storage::dynamic as storage; -// // Execute runtime API function call dynamically. -// pub use crate::runtime_api::dynamic as runtime_api_call; +/// 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 diff --git a/core/src/lib.rs b/core/src/lib.rs index a8b1af0795..fa2df7b65a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -13,9 +13,11 @@ extern crate alloc; pub mod client; pub mod config; pub mod constants; +pub mod custom_values; pub mod dynamic; mod error; pub mod metadata; +pub mod runtime_api; pub mod signer; pub mod storage; pub mod tx; diff --git a/core/src/metadata/metadata_type.rs b/core/src/metadata/metadata_type.rs index 07e74d9b36..4aaace7291 100644 --- a/core/src/metadata/metadata_type.rs +++ b/core/src/metadata/metadata_type.rs @@ -42,6 +42,11 @@ pub trait MetadatExt { &self, name: &str, ) -> Result; + + fn custom_value_by_name_err( + &self, + name: &str, + ) -> Result; } impl MetadatExt for subxt_metadata::Metadata { @@ -71,6 +76,16 @@ impl MetadatExt for subxt_metadata::Metadata { self.runtime_api_trait_by_name(name) .ok_or_else(|| MetadataError::RuntimeTraitNotFound(name.to_owned())) } + + /// Identical to `metadata.runtime_api_trait_by_name()`, but returns an error if the trait is not found. + fn custom_value_by_name_err( + &self, + name: &str, + ) -> Result { + self.custom() + .get(name) + .ok_or_else(|| MetadataError::CustomValueNameNotFound(name.to_owned())) + } } impl From for Metadata { diff --git a/core/src/runtime_api/mod.rs b/core/src/runtime_api/mod.rs new file mode 100644 index 0000000000..8f71771c68 --- /dev/null +++ b/core/src/runtime_api/mod.rs @@ -0,0 +1,189 @@ +// 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 alloc::borrow::Cow; +use alloc::borrow::ToOwned; +use alloc::string::String; +use alloc::vec::Vec; +use core::marker::PhantomData; +use derivative::Derivative; +use scale_encode::EncodeAsFields; +use scale_value::Composite; + +use crate::dynamic::DecodedValueThunk; +use crate::error::MetadataError; +use crate::Error; + +use crate::metadata::{DecodeWithMetadata, MetadatExt, 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 trait name. + fn trait_name(&self) -> &str; + + /// The runtime API method name. + fn method_name(&self) -> &str; + + /// Scale encode the arguments data. + fn encode_args_to(&self, metadata: &Metadata, out: &mut Vec) -> Result<(), Error>; + + /// Encode arguments data and return the output. This is a convenience + /// wrapper around [`RuntimeApiPayload::encode_args_to`]. + fn encode_args(&self, metadata: &Metadata) -> Result, Error> { + let mut v = Vec::new(); + self.encode_args_to(metadata, &mut v)?; + Ok(v) + } + + /// Returns the statically generated validation hash. + fn validation_hash(&self) -> Option<[u8; 32]> { + None + } +} + +/// A 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(Derivative)] +#[derivative( + Clone(bound = "ArgsData: Clone"), + Debug(bound = "ArgsData: core::fmt::Debug") +)] +pub struct Payload { + trait_name: Cow<'static, str>, + method_name: Cow<'static, str>, + args_data: ArgsData, + validation_hash: Option<[u8; 32]>, + _marker: PhantomData, +} + +impl RuntimeApiPayload + for Payload +{ + type ReturnType = ReturnTy; + + fn trait_name(&self) -> &str { + &self.trait_name + } + + fn method_name(&self) -> &str { + &self.method_name + } + + fn encode_args_to(&self, metadata: &Metadata, out: &mut Vec) -> Result<(), Error> { + let api_method = metadata + .runtime_api_trait_by_name_err(&self.trait_name)? + .method_by_name(&self.method_name) + .ok_or_else(|| MetadataError::RuntimeMethodNotFound((*self.method_name).to_owned()))?; + let mut fields = api_method + .inputs() + .map(|input| scale_encode::Field::named(input.ty, &input.name)); + + self.args_data + .encode_as_fields_to(&mut fields, metadata.types(), out)?; + Ok(()) + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + self.validation_hash + } +} + +/// A dynamic runtime API payload. +pub type DynamicRuntimeApiPayload = Payload, DecodedValueThunk>; + +impl Payload { + /// Create a new [`Payload`]. + pub fn new( + trait_name: impl Into, + method_name: impl Into, + args_data: ArgsData, + ) -> Self { + Payload { + trait_name: Cow::Owned(trait_name.into()), + method_name: Cow::Owned(method_name.into()), + args_data, + validation_hash: None, + _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( + trait_name: &'static str, + method_name: &'static str, + args_data: ArgsData, + hash: [u8; 32], + ) -> Payload { + Payload { + trait_name: Cow::Borrowed(trait_name), + method_name: Cow::Borrowed(method_name), + args_data, + validation_hash: Some(hash), + _marker: core::marker::PhantomData, + } + } + + /// Do not validate this call prior to submitting it. + pub fn unvalidated(self) -> Self { + Self { + validation_hash: None, + ..self + } + } + + /// Returns the trait name. + pub fn trait_name(&self) -> &str { + &self.trait_name + } + + /// Returns the method name. + pub fn method_name(&self) -> &str { + &self.method_name + } + + /// Returns the arguments data. + pub fn args_data(&self) -> &ArgsData { + &self.args_data + } +} + +/// Create a new [`DynamicRuntimeApiPayload`]. +pub fn dynamic( + trait_name: impl Into, + method_name: impl Into, + args_data: impl Into>, +) -> DynamicRuntimeApiPayload { + Payload::new(trait_name, method_name, args_data.into()) +} diff --git a/core/src/storage/mod.rs b/core/src/storage/mod.rs index 8bf68024e1..07b745d081 100644 --- a/core/src/storage/mod.rs +++ b/core/src/storage/mod.rs @@ -7,100 +7,8 @@ /// Types representing an address which describes where a storage /// entry lives and how to properly decode it. pub mod storage_address; +pub mod utils; // For consistency with other modules, also expose // the basic address stuff at the root of the module. pub use storage_address::{dynamic, Address, DynamicAddress, StorageAddress}; - -pub mod utils { - use crate::{ - metadata::{DecodeWithMetadata, MetadatExt}, - Error, Metadata, MetadataError, - }; - use alloc::borrow::ToOwned; - use alloc::vec::Vec; - use subxt_metadata::{PalletMetadata, StorageEntryMetadata}; - - use super::StorageAddress; - /// Return the root of a given [`StorageAddress`]: hash the pallet name and entry name - /// and append those bytes to the output. - pub fn write_storage_address_root_bytes( - addr: &Address, - out: &mut Vec, - ) { - out.extend(sp_core_hashing::twox_128(addr.pallet_name().as_bytes())); - out.extend(sp_core_hashing::twox_128(addr.entry_name().as_bytes())); - } - - /// Outputs the [`storage_address_root_bytes`] as well as any additional bytes that represent - /// a lookup in a storage map at that location. - pub fn storage_address_bytes( - addr: &Address, - metadata: &Metadata, - ) -> Result, Error> { - let mut bytes = Vec::new(); - write_storage_address_root_bytes(addr, &mut bytes); - addr.append_entry_bytes(metadata, &mut bytes)?; - Ok(bytes) - } - - /// Outputs a vector containing the bytes written by [`write_storage_address_root_bytes`]. - pub fn storage_address_root_bytes(addr: &Address) -> Vec { - let mut bytes = Vec::new(); - write_storage_address_root_bytes(addr, &mut bytes); - bytes - } - - /// Return details about the given storage entry. - pub fn lookup_entry_details<'a>( - pallet_name: &str, - entry_name: &str, - metadata: &'a subxt_metadata::Metadata, - ) -> Result<(PalletMetadata<'a>, &'a StorageEntryMetadata), Error> { - let pallet_metadata = metadata.pallet_by_name_err(pallet_name)?; - let storage_metadata = pallet_metadata - .storage() - .ok_or_else(|| MetadataError::StorageNotFoundInPallet(pallet_name.to_owned()))?; - let storage_entry = storage_metadata - .entry_by_name(entry_name) - .ok_or_else(|| MetadataError::StorageEntryNotFound(entry_name.to_owned()))?; - Ok((pallet_metadata, storage_entry)) - } - - /// Validate a storage address against the metadata. - pub fn validate_storage_address( - address: &Address, - pallet: PalletMetadata<'_>, - ) -> Result<(), Error> { - if let Some(hash) = address.validation_hash() { - validate_storage(pallet, address.entry_name(), hash)?; - } - Ok(()) - } - - /// Validate a storage entry against the metadata. - pub fn validate_storage( - pallet: PalletMetadata<'_>, - storage_name: &str, - hash: [u8; 32], - ) -> Result<(), Error> { - let Some(expected_hash) = pallet.storage_hash(storage_name) else { - return Err(MetadataError::IncompatibleCodegen.into()); - }; - if expected_hash != hash { - return Err(MetadataError::IncompatibleCodegen.into()); - } - Ok(()) - } - - /// Given some bytes, a pallet and storage name, decode the response. - pub fn decode_storage_with_metadata( - bytes: &mut &[u8], - metadata: &Metadata, - storage_metadata: &StorageEntryMetadata, - ) -> Result { - let return_ty = storage_metadata.entry_type().value_ty(); - let val = T::decode_with_metadata(bytes, return_ty, metadata)?; - Ok(val) - } -} diff --git a/core/src/storage/utils.rs b/core/src/storage/utils.rs new file mode 100644 index 0000000000..f4bfab8d29 --- /dev/null +++ b/core/src/storage/utils.rs @@ -0,0 +1,90 @@ +use crate::{ + metadata::{DecodeWithMetadata, MetadatExt}, + Error, Metadata, MetadataError, +}; +use alloc::borrow::ToOwned; +use alloc::vec::Vec; +use subxt_metadata::{PalletMetadata, StorageEntryMetadata}; + +use super::StorageAddress; +/// Return the root of a given [`StorageAddress`]: hash the pallet name and entry name +/// and append those bytes to the output. +pub fn write_storage_address_root_bytes( + addr: &Address, + out: &mut Vec, +) { + out.extend(sp_core_hashing::twox_128(addr.pallet_name().as_bytes())); + out.extend(sp_core_hashing::twox_128(addr.entry_name().as_bytes())); +} + +/// Outputs the [`storage_address_root_bytes`] as well as any additional bytes that represent +/// a lookup in a storage map at that location. +pub fn storage_address_bytes( + addr: &Address, + metadata: &Metadata, +) -> Result, Error> { + let mut bytes = Vec::new(); + write_storage_address_root_bytes(addr, &mut bytes); + addr.append_entry_bytes(metadata, &mut bytes)?; + Ok(bytes) +} + +/// Outputs a vector containing the bytes written by [`write_storage_address_root_bytes`]. +pub fn storage_address_root_bytes(addr: &Address) -> Vec { + let mut bytes = Vec::new(); + write_storage_address_root_bytes(addr, &mut bytes); + bytes +} + +/// Return details about the given storage entry. +pub fn lookup_entry_details<'a>( + pallet_name: &str, + entry_name: &str, + metadata: &'a subxt_metadata::Metadata, +) -> Result<(PalletMetadata<'a>, &'a StorageEntryMetadata), Error> { + let pallet_metadata = metadata.pallet_by_name_err(pallet_name)?; + let storage_metadata = pallet_metadata + .storage() + .ok_or_else(|| MetadataError::StorageNotFoundInPallet(pallet_name.to_owned()))?; + let storage_entry = storage_metadata + .entry_by_name(entry_name) + .ok_or_else(|| MetadataError::StorageEntryNotFound(entry_name.to_owned()))?; + Ok((pallet_metadata, storage_entry)) +} + +/// Validate a storage address against the metadata. +pub fn validate_storage_address( + address: &Address, + pallet: PalletMetadata<'_>, +) -> Result<(), Error> { + if let Some(hash) = address.validation_hash() { + validate_storage(pallet, address.entry_name(), hash)?; + } + Ok(()) +} + +/// Validate a storage entry against the metadata. +pub fn validate_storage( + pallet: PalletMetadata<'_>, + storage_name: &str, + hash: [u8; 32], +) -> Result<(), Error> { + let Some(expected_hash) = pallet.storage_hash(storage_name) else { + return Err(MetadataError::IncompatibleCodegen.into()); + }; + if expected_hash != hash { + return Err(MetadataError::IncompatibleCodegen.into()); + } + Ok(()) +} + +/// Given some bytes, a pallet and storage name, decode the response. +pub fn decode_storage_with_metadata( + bytes: &mut &[u8], + metadata: &Metadata, + storage_metadata: &StorageEntryMetadata, +) -> Result { + let return_ty = storage_metadata.entry_type().value_ty(); + let val = T::decode_with_metadata(bytes, return_ty, metadata)?; + Ok(val) +}