From 4d2b4a6755b03dacb8e348c1c1de9f7f33705d3d Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 9 Dec 2025 15:22:24 +0000 Subject: [PATCH] First pass adding back the main APIs --- new/src/client.rs | 22 ++++ new/src/constants.rs | 85 ++++++++++++++++ new/src/constants/address.rs | 137 +++++++++++++++++++++++++ new/src/custom_values.rs | 83 +++++++++++++++ new/src/custom_values/address.rs | 116 +++++++++++++++++++++ new/src/lib.rs | 10 +- new/src/runtime_apis.rs | 145 ++++++++++++++++++++++++++ new/src/runtime_apis/payload.rs | 161 +++++++++++++++++++++++++++++ new/src/storage.rs | 8 +- new/src/view_functions.rs | 142 ++++++++++++++++++++++++++ new/src/view_functions/payload.rs | 164 ++++++++++++++++++++++++++++++ 11 files changed, 1063 insertions(+), 10 deletions(-) create mode 100644 new/src/constants.rs create mode 100644 new/src/constants/address.rs create mode 100644 new/src/custom_values.rs create mode 100644 new/src/custom_values/address.rs create mode 100644 new/src/runtime_apis.rs create mode 100644 new/src/runtime_apis/payload.rs create mode 100644 new/src/view_functions.rs create mode 100644 new/src/view_functions/payload.rs diff --git a/new/src/client.rs b/new/src/client.rs index 4f5db17ba1..8c073e0222 100644 --- a/new/src/client.rs +++ b/new/src/client.rs @@ -2,11 +2,15 @@ mod offline_client; mod online_client; use crate::config::{Config, HashFor}; +use crate::constants::ConstantsClient; +use crate::custom_values::CustomValuesClient; use crate::error::{EventsError, ExtrinsicError}; use crate::events::Events; use crate::extrinsics::Extrinsics; +use crate::runtime_apis::RuntimeApisClient; use crate::storage::StorageClient; use crate::transactions::TransactionsClient; +use crate::view_functions::ViewFunctionsClient; use core::marker::PhantomData; use subxt_metadata::Metadata; @@ -45,6 +49,24 @@ where StorageClient::new(self.client.clone()) } + /// Access constants at this block. + pub fn constants(&self) -> ConstantsClient { + ConstantsClient::new(self.client.clone()) + } + + pub fn custom_values(&self) -> CustomValuesClient { + CustomValuesClient::new(self.client.clone()) + } + + /// Access runtime APIs at this block. + pub fn runtime_apis(&self) -> RuntimeApisClient { + RuntimeApisClient::new(self.client.clone()) + } + + pub fn view_functions(&self) -> ViewFunctionsClient { + ViewFunctionsClient::new(self.client.clone()) + } + /// Obtain a reference to the metadata. pub fn metadata_ref(&self) -> &Metadata { self.client.metadata_ref() diff --git a/new/src/constants.rs b/new/src/constants.rs new file mode 100644 index 0000000000..d26a24ff26 --- /dev/null +++ b/new/src/constants.rs @@ -0,0 +1,85 @@ +use crate::client::OfflineClientAtBlockT; +use crate::config::Config; +use crate::error::ConstantError; +use address::Address; +use frame_decode::constants::ConstantTypeInfo; +use scale_decode::IntoVisitor; +use std::marker::PhantomData; + +pub mod address; + +/// A client for working with storage entries. +#[derive(Clone)] +pub struct ConstantsClient { + client: Client, + marker: PhantomData, +} + +impl ConstantsClient { + pub(crate) fn new(client: Client) -> Self { + ConstantsClient { + client, + marker: PhantomData, + } + } +} + +impl> ConstantsClient { + /// Run the validation logic against some constant 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). + /// Return an error if the address was not valid or something went wrong trying to validate it (ie + /// the pallet or constant in question do not exist at all). + pub fn validate(&self, address: Addr) -> Result<(), ConstantError> { + let metadata = self.client.metadata_ref(); + if let Some(actual_hash) = address.validation_hash() { + let expected_hash = metadata + .pallet_by_name(address.pallet_name()) + .ok_or_else(|| { + ConstantError::PalletNameNotFound(address.pallet_name().to_string()) + })? + .constant_hash(address.constant_name()) + .ok_or_else(|| ConstantError::ConstantNameNotFound { + pallet_name: address.pallet_name().to_string(), + constant_name: address.constant_name().to_owned(), + })?; + if actual_hash != expected_hash { + return Err(ConstantError::IncompatibleCodegen); + } + } + Ok(()) + } + + /// Access the constant at the given address, returning the value defined by this address. + pub fn entry(&self, address: Addr) -> Result { + let metadata = self.client.metadata_ref(); + + // 1. Validate constant shape if hash given: + self.validate(&address)?; + + // 2. Attempt to decode the constant into the type given: + let constant = frame_decode::constants::decode_constant( + address.pallet_name(), + address.constant_name(), + metadata, + metadata.types(), + Addr::Target::into_visitor(), + ) + .map_err(ConstantError::CouldNotDecodeConstant)?; + + Ok(constant) + } + + /// Access the bytes of a constant by its address. + pub fn entry_bytes(&self, address: Addr) -> Result, ConstantError> { + // 1. Validate custom value shape if hash given: + self.validate(&address)?; + + // 2. Return the underlying bytes: + let constant = self + .client + .metadata_ref() + .constant_info(address.pallet_name(), address.constant_name()) + .map_err(|e| ConstantError::ConstantInfoError(e.into_owned()))?; + Ok(constant.bytes.to_vec()) + } +} diff --git a/new/src/constants/address.rs b/new/src/constants/address.rs new file mode 100644 index 0000000000..c54edec8f6 --- /dev/null +++ b/new/src/constants/address.rs @@ -0,0 +1,137 @@ +// Copyright 2019-2024 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! Construct addresses to access constants with. + +use derive_where::derive_where; +use scale_decode::DecodeAsType; +use std::borrow::Cow; + +/// This represents a constant address. Anything implementing this trait +/// can be used to fetch constants. +pub trait Address { + /// The target type of the value that lives at this address. + type Target: DecodeAsType; + + /// The name of the pallet that the constant lives under. + fn pallet_name(&self) -> &str; + + /// The name of the constant in a given pallet. + fn constant_name(&self) -> &str; + + /// An optional hash which, if present, will be checked against + /// the node metadata to confirm that the return type matches what + /// we are expecting. + fn validation_hash(&self) -> Option<[u8; 32]> { + None + } +} + +// Any reference to an address is a valid address. +impl Address for &'_ A { + type Target = A::Target; + + fn pallet_name(&self) -> &str { + A::pallet_name(*self) + } + + fn constant_name(&self) -> &str { + A::constant_name(*self) + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + A::validation_hash(*self) + } +} + +// (str, str) and similar are valid addresses. +impl, B: AsRef> Address for (A, B) { + type Target = scale_value::Value; + + fn pallet_name(&self) -> &str { + self.0.as_ref() + } + + fn constant_name(&self) -> &str { + self.1.as_ref() + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + None + } +} + +/// This represents the address of a constant. +#[derive_where(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct StaticAddress { + pallet_name: Cow<'static, str>, + constant_name: Cow<'static, str>, + constant_hash: Option<[u8; 32]>, + _marker: core::marker::PhantomData, +} + +/// A dynamic lookup address to access a constant. +pub type DynamicAddress = StaticAddress; + +impl StaticAddress { + /// Create a new [`StaticAddress`] to use to look up a constant. + pub fn new(pallet_name: impl Into, constant_name: impl Into) -> Self { + Self { + pallet_name: Cow::Owned(pallet_name.into()), + constant_name: Cow::Owned(constant_name.into()), + constant_hash: None, + _marker: core::marker::PhantomData, + } + } + + /// Create a new [`StaticAddress`] that will be validated + /// against node metadata using the hash given. + #[doc(hidden)] + pub fn new_static( + pallet_name: &'static str, + constant_name: &'static str, + hash: [u8; 32], + ) -> Self { + Self { + pallet_name: Cow::Borrowed(pallet_name), + constant_name: Cow::Borrowed(constant_name), + constant_hash: Some(hash), + _marker: core::marker::PhantomData, + } + } + + /// Do not validate this constant prior to accessing it. + pub fn unvalidated(self) -> Self { + Self { + pallet_name: self.pallet_name, + constant_name: self.constant_name, + constant_hash: None, + _marker: self._marker, + } + } +} + +impl Address for StaticAddress { + type Target = ReturnTy; + + fn pallet_name(&self) -> &str { + &self.pallet_name + } + + fn constant_name(&self) -> &str { + &self.constant_name + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + self.constant_hash + } +} + +/// Construct a new dynamic constant lookup. +pub fn dynamic( + pallet_name: impl Into, + constant_name: impl Into, +) -> DynamicAddress { + DynamicAddress::new(pallet_name, constant_name) +} diff --git a/new/src/custom_values.rs b/new/src/custom_values.rs new file mode 100644 index 0000000000..dc785cb012 --- /dev/null +++ b/new/src/custom_values.rs @@ -0,0 +1,83 @@ +use crate::client::OfflineClientAtBlockT; +use crate::config::Config; +use crate::error::CustomValueError; +use crate::utils::Maybe; +use address::Address; +use derive_where::derive_where; +use frame_decode::custom_values::CustomValueTypeInfo; +use scale_decode::IntoVisitor; + +pub mod address; + +/// A client for accessing custom values stored in the metadata. +#[derive_where(Clone; Client)] +pub struct CustomValuesClient { + client: Client, + marker: std::marker::PhantomData, +} + +impl CustomValuesClient { + /// Create a new [`CustomValuesClient`]. + pub(crate) fn new(client: Client) -> Self { + Self { + client, + marker: std::marker::PhantomData, + } + } +} + +impl> CustomValuesClient { + /// 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(&self, address: Addr) -> Result<(), CustomValueError> { + let metadata = self.client.metadata_ref(); + if let Some(actual_hash) = address.validation_hash() { + let custom = metadata.custom(); + let custom_value = custom + .get(address.name()) + .ok_or_else(|| CustomValueError::NotFound(address.name().into()))?; + let expected_hash = custom_value.hash(); + if actual_hash != expected_hash { + return Err(CustomValueError::IncompatibleCodegen); + } + } + 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 entry>( + &self, + address: Addr, + ) -> Result { + // 1. Validate custom value shape if hash given: + self.validate(&address)?; + + // 2. Attempt to decode custom value: + let metadata = self.client.metadata_ref(); + let value = frame_decode::custom_values::decode_custom_value( + address.name(), + metadata, + metadata.types(), + Addr::Target::into_visitor(), + ) + .map_err(CustomValueError::CouldNotDecodeCustomValue)?; + + Ok(value) + } + + /// Access the bytes of a custom value by the address it is registered under. + pub fn entry_bytes(&self, address: Addr) -> Result, CustomValueError> { + // 1. Validate custom value shape if hash given: + self.validate(&address)?; + + // 2. Return the underlying bytes: + let custom_value = self + .client + .metadata_ref() + .custom_value_info(address.name()) + .map_err(|e| CustomValueError::NotFound(e.not_found))?; + Ok(custom_value.bytes.to_vec()) + } +} diff --git a/new/src/custom_values/address.rs b/new/src/custom_values/address.rs new file mode 100644 index 0000000000..8d5cbcc275 --- /dev/null +++ b/new/src/custom_values/address.rs @@ -0,0 +1,116 @@ +// Copyright 2019-2024 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! Construct addresses to access custom values with. + +use derive_where::derive_where; +use scale_decode::DecodeAsType; +use std::borrow::Cow; + +/// Use this with [`Address::IsDecodable`]. +pub use crate::utils::{Maybe, No, NoMaybe}; + +/// This represents the address of a custom value in the metadata. +/// Anything that implements it can be used to fetch custom values from the metadata. +/// The trait is implemented by [`str`] for dynamic lookup and [`StaticAddress`] for static queries. +pub trait Address { + /// The type of the custom value. + type Target: DecodeAsType; + /// Should be set to `Yes` for Dynamic values and static values that have a valid type. + /// Should be `No` for custom values, that have an invalid type id. + type IsDecodable: NoMaybe; + + /// 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 + } +} + +// Any reference to an address is a valid address +impl Address for &'_ A { + type Target = A::Target; + type IsDecodable = A::IsDecodable; + + fn name(&self) -> &str { + A::name(*self) + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + A::validation_hash(*self) + } +} + +// Support plain strings for looking up custom values. +impl Address for str { + type Target = scale_value::Value; + type IsDecodable = Maybe; + + fn name(&self) -> &str { + self + } +} + +/// A static address to a custom value. +#[derive_where(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct StaticAddress { + name: Cow<'static, str>, + hash: Option<[u8; 32]>, + marker: core::marker::PhantomData<(ReturnTy, IsDecodable)>, +} + +/// A dynamic address to a custom value. +pub type DynamicAddress = StaticAddress; + +impl StaticAddress { + #[doc(hidden)] + /// Creates a new StaticAddress. + pub fn new_static(name: &'static str, hash: [u8; 32]) -> Self { + Self { + name: Cow::Borrowed(name), + hash: Some(hash), + marker: core::marker::PhantomData, + } + } + + /// Create a new [`StaticAddress`] + pub fn new(name: impl Into) -> Self { + Self { + name: name.into().into(), + hash: None, + marker: core::marker::PhantomData, + } + } + + /// Do not validate this custom value prior to accessing it. + pub fn unvalidated(self) -> Self { + Self { + name: self.name, + hash: None, + marker: self.marker, + } + } +} + +impl Address for StaticAddress { + type Target = Target; + type IsDecodable = IsDecodable; + + fn name(&self) -> &str { + &self.name + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + self.hash + } +} + +/// Construct a new dynamic custom value lookup. +pub fn dynamic( + custom_value_name: impl Into, +) -> DynamicAddress { + DynamicAddress::new(custom_value_name) +} diff --git a/new/src/lib.rs b/new/src/lib.rs index c4ebd23942..60ffe5bbc5 100644 --- a/new/src/lib.rs +++ b/new/src/lib.rs @@ -35,20 +35,18 @@ mod only_used_in_docs_or_tests { pub mod backend; pub mod client; pub mod config; +pub mod constants; +pub mod custom_values; pub mod error; pub mod events; pub mod extrinsics; +pub mod runtime_apis; pub mod storage; pub mod transactions; pub mod utils; +pub mod view_functions; // pub mod book; // pub mod blocks; -// pub mod constants; -// pub mod custom_values; -// pub mod runtime_api; -// pub mod storage; -// pub mod tx; -// pub mod view_functions; // /// This module provides a [`Config`] type, which is used to define various // /// types that are important in order to speak to a particular chain. diff --git a/new/src/runtime_apis.rs b/new/src/runtime_apis.rs new file mode 100644 index 0000000000..49fdb12ab9 --- /dev/null +++ b/new/src/runtime_apis.rs @@ -0,0 +1,145 @@ +// Copyright 2019-2025 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT}; +use crate::config::Config; +use crate::error::RuntimeApiError; +use derive_where::derive_where; +use payload::Payload; +use scale_decode::IntoVisitor; +use std::marker::PhantomData; + +pub mod payload; + +/// Execute runtime API calls. +#[derive_where(Clone; Client)] +pub struct RuntimeApisClient { + client: Client, + marker: PhantomData, +} + +impl RuntimeApisClient { + /// Create a new [`RuntimeApi`] + pub(crate) fn new(client: Client) -> Self { + Self { + client, + marker: PhantomData, + } + } +} + +impl RuntimeApisClient +where + T: Config, + Client: OfflineClientAtBlockT, +{ + /// Run the validation logic against some runtime API payload you'd like to use. Returns `Ok(())` + /// if the payload is valid (or if it's not possible to check since the payload has no validation hash). + /// Return an error if the payload was not valid or something went wrong trying to validate it (ie + /// the runtime API in question do not exist at all) + pub fn validate(&self, payload: P) -> Result<(), RuntimeApiError> { + let Some(hash) = payload.validation_hash() else { + return Ok(()); + }; + + let metadata = self.client.metadata_ref(); + let trait_name = payload.trait_name(); + let method_name = payload.method_name(); + + let api_trait = metadata + .runtime_api_trait_by_name(trait_name) + .ok_or_else(|| RuntimeApiError::TraitNotFound(trait_name.to_string()))?; + let api_method = api_trait.method_by_name(method_name).ok_or_else(|| { + RuntimeApiError::MethodNotFound { + trait_name: trait_name.to_string(), + method_name: method_name.to_string(), + } + })?; + + if hash != api_method.hash() { + Err(RuntimeApiError::IncompatibleCodegen) + } else { + Ok(()) + } + } + + /// Return the name of the runtime API call from the payload. + pub fn encode_name(&self, payload: P) -> String { + format!("{}_{}", payload.trait_name(), payload.method_name()) + } + + /// Return the encoded call args from a runtime API payload. + pub fn encode_args(&self, payload: P) -> Result, RuntimeApiError> { + let metadata = self.client.metadata_ref(); + let value = frame_decode::runtime_apis::encode_runtime_api_inputs( + payload.trait_name(), + payload.method_name(), + payload.args(), + metadata, + metadata.types(), + ) + .map_err(RuntimeApiError::CouldNotEncodeInputs)?; + + Ok(value) + } +} + +impl RuntimeApisClient +where + T: Config, + Client: OnlineClientAtBlockT, +{ + /// Execute a raw runtime API call. This returns the raw bytes representing the result + /// of this call. The caller is responsible for decoding the result. + pub async fn call_raw<'a>( + &self, + function: &'a str, + call_parameters: Option<&'a [u8]>, + ) -> Result, RuntimeApiError> { + let client = &self.client; + let block_hash = client.block_hash(); + let data = client + .backend() + .call(function, call_parameters, block_hash) + .await + .map_err(RuntimeApiError::CannotCallApi)?; + + Ok(data) + } + + /// Execute a runtime API call. + pub async fn call(&self, payload: P) -> Result { + let client = &self.client; + let block_hash = client.block_hash(); + let metadata = client.metadata_ref(); + + // Validate the runtime API payload hash against the compile hash from codegen. + self.validate(&payload)?; + + // Encode the arguments of the runtime call. + let call_name = self.encode_name(&payload); + let call_args = self.encode_args(&payload)?; + + // Make the call. + let bytes = client + .backend() + .call(&call_name, Some(call_args.as_slice()), block_hash) + .await + .map_err(RuntimeApiError::CannotCallApi)?; + + // Decode the response. + let cursor = &mut &*bytes; + let value = frame_decode::runtime_apis::decode_runtime_api_response( + payload.trait_name(), + payload.method_name(), + cursor, + metadata, + metadata.types(), + P::ReturnType::into_visitor(), + ) + .map_err(RuntimeApiError::CouldNotDecodeResponse)?; + + Ok(value) + } +} diff --git a/new/src/runtime_apis/payload.rs b/new/src/runtime_apis/payload.rs new file mode 100644 index 0000000000..c62da4225b --- /dev/null +++ b/new/src/runtime_apis/payload.rs @@ -0,0 +1,161 @@ +// Copyright 2019-2024 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! This module contains the trait and types used to represent +//! runtime API calls that can be made. + +use core::marker::PhantomData; +use derive_where::derive_where; +use frame_decode::runtime_apis::IntoEncodableValues; +use scale_decode::DecodeAsType; +use std::borrow::Cow; + +/// This represents a runtime API payload that can be used to call a Runtime API on +/// a chain and decode the response. +pub trait Payload { + /// Type of the arguments. + type ArgsType: IntoEncodableValues; + /// The return type of the function call. + type ReturnType: DecodeAsType; + + /// The runtime API trait name. + fn trait_name(&self) -> &str; + + /// The runtime API method name. + fn method_name(&self) -> &str; + + /// The input arguments. + fn args(&self) -> &Self::ArgsType; + + /// Returns the statically generated validation hash. + fn validation_hash(&self) -> Option<[u8; 32]> { + None + } +} + +// Any reference to a payload is a valid payload. +impl Payload for &'_ P { + type ArgsType = P::ArgsType; + type ReturnType = P::ReturnType; + + fn trait_name(&self) -> &str { + P::trait_name(*self) + } + + fn method_name(&self) -> &str { + P::method_name(*self) + } + + fn args(&self) -> &Self::ArgsType { + P::args(*self) + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + P::validation_hash(*self) + } +} + +/// 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_where(Clone, Debug, Eq, Ord, PartialEq, PartialOrd; ArgsType)] +pub struct StaticPayload { + trait_name: Cow<'static, str>, + method_name: Cow<'static, str>, + args: ArgsType, + validation_hash: Option<[u8; 32]>, + _marker: PhantomData, +} + +/// A dynamic runtime API payload. +pub type DynamicPayload = StaticPayload; + +impl Payload + for StaticPayload +{ + type ArgsType = ArgsType; + type ReturnType = ReturnType; + + fn trait_name(&self) -> &str { + &self.trait_name + } + + fn method_name(&self) -> &str { + &self.method_name + } + + fn args(&self) -> &Self::ArgsType { + &self.args + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + self.validation_hash + } +} + +impl StaticPayload { + /// Create a new [`StaticPayload`]. + pub fn new( + trait_name: impl Into, + method_name: impl Into, + args: ArgsType, + ) -> Self { + StaticPayload { + trait_name: trait_name.into().into(), + method_name: method_name.into().into(), + args, + validation_hash: None, + _marker: PhantomData, + } + } + + /// Create a new static [`StaticPayload`] 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: ArgsType, + hash: [u8; 32], + ) -> StaticPayload { + StaticPayload { + trait_name: Cow::Borrowed(trait_name), + method_name: Cow::Borrowed(method_name), + args, + 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 + } +} + +/// Create a new [`DynamicPayload`]. +pub fn dynamic( + trait_name: impl Into, + method_name: impl Into, + args_data: ArgsType, +) -> DynamicPayload { + DynamicPayload::new(trait_name, method_name, args_data) +} diff --git a/new/src/storage.rs b/new/src/storage.rs index d0cfc6681e..debdf2e1af 100644 --- a/new/src/storage.rs +++ b/new/src/storage.rs @@ -21,7 +21,7 @@ pub use storage_key_value::StorageKeyValue; pub use storage_value::StorageValue; pub mod address; -/// A client for working with transactions. +/// A client for working with storage entries. #[derive(Clone)] pub struct StorageClient { client: Client, @@ -81,7 +81,7 @@ impl> StorageClient { /// Iterate over all of the storage entries listed in the metadata for the current block. This does **not** include well known /// storage entries like `:code` which are not listed in the metadata. - pub fn entries(&self) -> impl Iterator> { + pub fn entries(&self) -> impl Iterator> { let metadata = self.client.metadata_ref(); Entry::tuples_of(metadata.storage_entries()).map(|(pallet_name, entry_name)| { StorageEntryRef { @@ -173,14 +173,14 @@ impl> StorageClient { } /// Working with a specific storage entry. -pub struct StorageEntryRef<'atblock, Client, T> { +pub struct StorageEntryRef<'atblock, T, Client> { pallet_name: Cow<'atblock, str>, entry_name: Cow<'atblock, str>, client: &'atblock Client, marker: std::marker::PhantomData, } -impl<'atblock, Client, T> StorageEntryRef<'atblock, Client, T> +impl<'atblock, Client, T> StorageEntryRef<'atblock, T, Client> where T: Config, Client: OfflineClientAtBlockT, diff --git a/new/src/view_functions.rs b/new/src/view_functions.rs new file mode 100644 index 0000000000..3a89d55938 --- /dev/null +++ b/new/src/view_functions.rs @@ -0,0 +1,142 @@ +// Copyright 2019-2025 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT}; +use crate::config::Config; +use crate::error::ViewFunctionError; +use derive_where::derive_where; +use payload::Payload; +use scale_decode::IntoVisitor; +use std::marker::PhantomData; + +pub mod payload; + +/// The name of the Runtime API call which can execute +const CALL_NAME: &str = "RuntimeViewFunction_execute_view_function"; + +/// Execute View Function calls. +#[derive_where(Clone; Client)] +pub struct ViewFunctionsClient { + client: Client, + marker: PhantomData, +} + +impl ViewFunctionsClient { + /// Create a new [`ViewFunctionsClient`] + pub(crate) fn new(client: Client) -> Self { + Self { + client, + marker: PhantomData, + } + } +} + +impl ViewFunctionsClient +where + T: Config, + Client: OfflineClientAtBlockT, +{ + /// Run the validation logic against some View Function payload you'd like to use. Returns `Ok(())` + /// if the payload is valid (or if it's not possible to check since the payload has no validation hash). + /// Return an error if the payload was not valid or something went wrong trying to validate it (ie + /// the View Function in question do not exist at all) + pub fn validate(&self, payload: Call) -> Result<(), ViewFunctionError> { + let Some(hash) = payload.validation_hash() else { + return Ok(()); + }; + + let metadata = self.client.metadata_ref(); + let pallet_name = payload.pallet_name(); + let function_name = payload.function_name(); + + let view_function = metadata + .pallet_by_name(pallet_name) + .ok_or_else(|| ViewFunctionError::PalletNotFound(pallet_name.to_string()))? + .view_function_by_name(function_name) + .ok_or_else(|| ViewFunctionError::ViewFunctionNotFound { + pallet_name: pallet_name.to_string(), + function_name: function_name.to_string(), + })?; + + if hash != view_function.hash() { + Err(ViewFunctionError::IncompatibleCodegen) + } else { + Ok(()) + } + } + + /// Encode the bytes that will be passed to the "execute_view_function" Runtime API call, + /// to execute the View Function represented by the given payload. + pub fn encode_args(&self, payload: P) -> Result, ViewFunctionError> { + let metadata = self.client.metadata_ref(); + let inputs = frame_decode::view_functions::encode_view_function_inputs( + payload.pallet_name(), + payload.function_name(), + payload.args(), + metadata, + metadata.types(), + ) + .map_err(ViewFunctionError::CouldNotEncodeInputs)?; + + Ok(inputs) + } +} + +impl ViewFunctionsClient +where + T: Config, + Client: OnlineClientAtBlockT, +{ + /// Execute a raw View function API call. This returns the raw bytes representing the result + /// of this call. The caller is responsible for decoding the result. + pub async fn call_raw<'a>( + &self, + call_parameters: Option<&'a [u8]>, + ) -> Result, ViewFunctionError> { + let client = &self.client; + let block_hash = client.block_hash(); + let data = client + .backend() + .call(CALL_NAME, call_parameters, block_hash) + .await + .map_err(ViewFunctionError::CannotCallApi)?; + + Ok(data) + } + + /// Execute a View Function call. + pub async fn call(&self, payload: P) -> Result { + let client = &self.client; + let metadata = client.metadata_ref(); + let block_hash = client.block_hash(); + + // Validate the View Function payload hash against the compile hash from codegen. + self.validate(&payload)?; + + // Assemble the data to call the "execute_view_function" runtime API, which + // then calls the relevant view function. + let call_args = self.encode_args(&payload)?; + + // Make the call. + let bytes = client + .backend() + .call(CALL_NAME, Some(call_args.as_slice()), block_hash) + .await + .map_err(ViewFunctionError::CannotCallApi)?; + + // Decode the response. + let cursor = &mut &*bytes; + let value = frame_decode::view_functions::decode_view_function_response( + payload.pallet_name(), + payload.function_name(), + cursor, + metadata, + metadata.types(), + P::ReturnType::into_visitor(), + ) + .map_err(ViewFunctionError::CouldNotDecodeResponse)?; + + Ok(value) + } +} diff --git a/new/src/view_functions/payload.rs b/new/src/view_functions/payload.rs new file mode 100644 index 0000000000..48ed0a43ca --- /dev/null +++ b/new/src/view_functions/payload.rs @@ -0,0 +1,164 @@ +// Copyright 2019-2024 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! This module contains the trait and types used to represent +//! View Function calls that can be made. + +use core::marker::PhantomData; +use derive_where::derive_where; +use frame_decode::view_functions::IntoEncodableValues; +use scale_decode::DecodeAsType; +use std::borrow::Cow; + +/// This represents a View Function payload that can call into the runtime of node. +/// +/// # Components +/// +/// - associated return type +/// +/// Resulting bytes of the call are interpreted into this type. +/// +/// - query ID +/// +/// The ID used to identify in the runtime which view function to call. +/// +/// - encoded arguments +/// +/// Each argument of the View Function must be scale-encoded. +pub trait Payload { + /// Type of the arguments for this call. + type ArgsType: IntoEncodableValues; + /// The return type of the function call. + type ReturnType: DecodeAsType; + + /// The View Function pallet name. + fn pallet_name(&self) -> &str; + + /// The View Function function name. + fn function_name(&self) -> &str; + + /// The arguments. + fn args(&self) -> &Self::ArgsType; + + /// Returns the statically generated validation hash. + fn validation_hash(&self) -> Option<[u8; 32]> { + None + } +} + +// A reference to a payload is a valid payload. +impl Payload for &'_ P { + type ArgsType = P::ArgsType; + type ReturnType = P::ReturnType; + + fn pallet_name(&self) -> &str { + P::pallet_name(*self) + } + + fn function_name(&self) -> &str { + P::function_name(*self) + } + + fn args(&self) -> &Self::ArgsType { + P::args(*self) + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + P::validation_hash(*self) + } +} + +/// A View Function payload containing the generic argument data +/// and interpreting the result of the call as `ReturnType`. +/// +/// This can be created from static values (ie those generated +/// via the `subxt` macro) or dynamic values via [`dynamic`]. +#[derive_where(Clone, Debug, Eq, Ord, PartialEq, PartialOrd; ArgsType)] +pub struct StaticPayload { + pallet_name: Cow<'static, str>, + function_name: Cow<'static, str>, + args: ArgsType, + validation_hash: Option<[u8; 32]>, + _marker: PhantomData, +} + +/// A dynamic View Function payload. +pub type DynamicPayload = StaticPayload; + +impl Payload + for StaticPayload +{ + type ArgsType = ArgsType; + type ReturnType = ReturnType; + + fn pallet_name(&self) -> &str { + &self.pallet_name + } + + fn function_name(&self) -> &str { + &self.function_name + } + + fn args(&self) -> &Self::ArgsType { + &self.args + } + + fn validation_hash(&self) -> Option<[u8; 32]> { + self.validation_hash + } +} + +impl StaticPayload { + /// Create a new [`StaticPayload`] for a View Function call. + pub fn new( + pallet_name: impl Into, + function_name: impl Into, + args: ArgsType, + ) -> Self { + StaticPayload { + pallet_name: pallet_name.into().into(), + function_name: function_name.into().into(), + args, + validation_hash: None, + _marker: PhantomData, + } + } + + /// Create a new static [`StaticPayload`] for a View Function call + /// using static function name and scale-encoded argument data. + /// + /// This is only expected to be used from codegen. + #[doc(hidden)] + pub fn new_static( + pallet_name: &'static str, + function_name: &'static str, + args: ArgsType, + hash: [u8; 32], + ) -> StaticPayload { + StaticPayload { + pallet_name: Cow::Borrowed(pallet_name), + function_name: Cow::Borrowed(function_name), + args, + 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 + } + } +} + +/// Create a new [`DynamicPayload`] to call a View Function. +pub fn dynamic( + pallet_name: impl Into, + function_name: impl Into, + args: ArgsType, +) -> DynamicPayload { + DynamicPayload::new(pallet_name, function_name, args) +}