diff --git a/subxt/src/blocks/block_types.rs b/subxt/src/blocks/block_types.rs index 9adba3b8ea..82372974ba 100644 --- a/subxt/src/blocks/block_types.rs +++ b/subxt/src/blocks/block_types.rs @@ -18,6 +18,7 @@ use crate::{ }, events, rpc::types::ChainBlockResponse, + runtime_api::RuntimeApi, }; use derivative::Derivative; use futures::lock::Mutex as AsyncMutex; @@ -89,6 +90,11 @@ where self.cached_events.clone(), )) } + + /// Execute a runtime API call at this block. + pub async fn runtime_api(&self) -> Result, Error> { + Ok(RuntimeApi::new(self.client.clone(), self.hash())) + } } /// The body of a block. diff --git a/subxt/src/client/offline_client.rs b/subxt/src/client/offline_client.rs index 4055787f98..c2681102f5 100644 --- a/subxt/src/client/offline_client.rs +++ b/subxt/src/client/offline_client.rs @@ -7,6 +7,7 @@ use crate::{ constants::ConstantsClient, events::EventsClient, rpc::types::RuntimeVersion, + runtime_api::RuntimeApiClient, storage::StorageClient, tx::TxClient, Config, @@ -49,6 +50,11 @@ pub trait OfflineClientT: Clone + Send + Sync + 'static { fn blocks(&self) -> BlocksClient { BlocksClient::new(self.clone()) } + + /// Work with runtime API. + fn runtime_api(&self) -> RuntimeApiClient { + RuntimeApiClient::new(self.clone()) + } } /// A client that is capable of performing offline-only operations. diff --git a/subxt/src/client/online_client.rs b/subxt/src/client/online_client.rs index 0fea1591a4..96433a7b50 100644 --- a/subxt/src/client/online_client.rs +++ b/subxt/src/client/online_client.rs @@ -19,12 +19,18 @@ use crate::{ Rpc, RpcClientT, }, + runtime_api::RuntimeApiClient, storage::StorageClient, tx::TxClient, Config, Metadata, }; +use codec::{ + Compact, + Decode, +}; use derivative::Derivative; +use frame_metadata::RuntimeMetadataPrefixed; use futures::future; use parking_lot::RwLock; use std::sync::Arc; @@ -95,7 +101,7 @@ impl OnlineClient { let (genesis_hash, runtime_version, metadata) = future::join3( rpc.genesis_hash(), rpc.runtime_version(None), - rpc.metadata(None), + OnlineClient::fetch_metadata(&rpc), ) .await; @@ -109,6 +115,15 @@ impl OnlineClient { }) } + /// Fetch the metadata from substrate using the runtime API. + async fn fetch_metadata(rpc: &Rpc) -> Result { + let bytes = rpc.state_call("Metadata_metadata", None, None).await?; + let cursor = &mut &*bytes; + let _ = >::decode(cursor)?; + let meta: RuntimeMetadataPrefixed = Decode::decode(cursor)?; + Ok(meta.try_into()?) + } + /// Create an object which can be used to keep the runtime up to date /// in a separate thread. /// @@ -214,6 +229,11 @@ impl OnlineClient { pub fn blocks(&self) -> BlocksClient { >::blocks(self) } + + /// Work with runtime API. + pub fn runtime_api(&self) -> RuntimeApiClient { + >::runtime_api(self) + } } impl OfflineClientT for OnlineClient { diff --git a/subxt/src/lib.rs b/subxt/src/lib.rs index b9b46fb85c..4a37a59751 100644 --- a/subxt/src/lib.rs +++ b/subxt/src/lib.rs @@ -155,6 +155,7 @@ pub mod error; pub mod events; pub mod metadata; pub mod rpc; +pub mod runtime_api; pub mod storage; pub mod tx; pub mod utils; diff --git a/subxt/src/rpc/rpc.rs b/subxt/src/rpc/rpc.rs index b8d1c1b486..fee29799d4 100644 --- a/subxt/src/rpc/rpc.rs +++ b/subxt/src/rpc/rpc.rs @@ -371,6 +371,25 @@ impl Rpc { Ok(xt_hash) } + /// Execute a runtime API call. + pub async fn state_call( + &self, + function: &str, + call_parameters: Option<&[u8]>, + at: Option, + ) -> Result { + let call_parameters = call_parameters.unwrap_or_default(); + + let bytes: types::Bytes = self + .client + .request( + "state_call", + rpc_params![function, to_hex(call_parameters), at], + ) + .await?; + Ok(bytes) + } + /// Create and submit an extrinsic and return a subscription to the events triggered. pub async fn watch_extrinsic( &self, diff --git a/subxt/src/runtime_api/mod.rs b/subxt/src/runtime_api/mod.rs new file mode 100644 index 0000000000..7b55a3905a --- /dev/null +++ b/subxt/src/runtime_api/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2022 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! Types associated with executing runtime API calls. + +mod runtime_client; +mod runtime_types; + +pub use runtime_client::RuntimeApiClient; +pub use runtime_types::RuntimeApi; diff --git a/subxt/src/runtime_api/runtime_client.rs b/subxt/src/runtime_api/runtime_client.rs new file mode 100644 index 0000000000..745bd4f820 --- /dev/null +++ b/subxt/src/runtime_api/runtime_client.rs @@ -0,0 +1,66 @@ +// Copyright 2019-2022 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +use super::runtime_types::RuntimeApi; + +use crate::{ + client::OnlineClientT, + error::Error, + Config, +}; +use derivative::Derivative; +use std::{ + future::Future, + marker::PhantomData, +}; + +/// Execute runtime API calls. +#[derive(Derivative)] +#[derivative(Clone(bound = "Client: Clone"))] +pub struct RuntimeApiClient { + client: Client, + _marker: PhantomData, +} + +impl RuntimeApiClient { + /// Create a new [`RuntimeApiClient`] + pub fn new(client: Client) -> Self { + Self { + client, + _marker: PhantomData, + } + } +} + +impl RuntimeApiClient +where + T: Config, + Client: OnlineClientT, +{ + /// Obtain a runtime API at some block hash. + pub fn at( + &self, + block_hash: Option, + ) -> impl Future, Error>> + Send + 'static { + // Clone and pass the client in like this so that we can explicitly + // return a Future that's Send + 'static, rather than tied to &self. + let client = self.client.clone(); + async move { + // If block hash is not provided, get the hash + // for the latest block and use that. + let block_hash = match block_hash { + Some(hash) => hash, + None => { + client + .rpc() + .block_hash(None) + .await? + .expect("substrate RPC returns the best block when no block number is provided; qed") + } + }; + + Ok(RuntimeApi::new(client, block_hash)) + } + } +} diff --git a/subxt/src/runtime_api/runtime_types.rs b/subxt/src/runtime_api/runtime_types.rs new file mode 100644 index 0000000000..a449c0e377 --- /dev/null +++ b/subxt/src/runtime_api/runtime_types.rs @@ -0,0 +1,59 @@ +// Copyright 2019-2022 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::OnlineClientT, + error::Error, + Config, +}; +use derivative::Derivative; +use std::{ + future::Future, + marker::PhantomData, +}; + +/// Execute runtime API calls. +#[derive(Derivative)] +#[derivative(Clone(bound = "Client: Clone"))] +pub struct RuntimeApi { + client: Client, + block_hash: T::Hash, + _marker: PhantomData, +} + +impl RuntimeApi { + /// Create a new [`RuntimeApi`] + pub(crate) fn new(client: Client, block_hash: T::Hash) -> Self { + Self { + client, + block_hash, + _marker: PhantomData, + } + } +} + +impl RuntimeApi +where + T: Config, + Client: OnlineClientT, +{ + /// Execute a raw runtime API call. + pub fn call_raw<'a>( + &self, + function: &'a str, + call_parameters: Option<&'a [u8]>, + ) -> impl Future, Error>> + 'a { + 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 data = client + .rpc() + .state_call(function, call_parameters, Some(block_hash)) + .await?; + Ok(data.0) + } + } +} diff --git a/testing/integration-tests/src/blocks/mod.rs b/testing/integration-tests/src/blocks/mod.rs index 71ddc257eb..a3730752fd 100644 --- a/testing/integration-tests/src/blocks/mod.rs +++ b/testing/integration-tests/src/blocks/mod.rs @@ -3,6 +3,11 @@ // see LICENSE for license details. use crate::test_context; +use codec::{ + Compact, + Decode, +}; +use frame_metadata::RuntimeMetadataPrefixed; use futures::StreamExt; // Check that we can subscribe to non-finalized blocks. @@ -87,3 +92,30 @@ async fn missing_block_headers_will_be_filled_in() -> Result<(), subxt::Error> { assert!(last_block_number.is_some()); Ok(()) } + +// Check that we can subscribe to non-finalized blocks. +#[tokio::test] +async fn runtime_api_call() -> Result<(), subxt::Error> { + let ctx = test_context().await; + let api = ctx.client(); + + let mut sub = api.blocks().subscribe_best().await?; + + let block = sub.next().await.unwrap()?; + let rt = block.runtime_api().await?; + + let bytes = rt.call_raw("Metadata_metadata", None).await?; + let cursor = &mut &*bytes; + let _ = >::decode(cursor)?; + let meta: RuntimeMetadataPrefixed = Decode::decode(cursor)?; + let metadata_call = match meta.1 { + frame_metadata::RuntimeMetadata::V14(metadata) => metadata, + _ => panic!("Metadata V14 unavailable"), + }; + + // Compare the runtime API call against the `state_getMetadata`. + let metadata = api.rpc().metadata(None).await?; + let metadata = metadata.runtime_metadata(); + assert_eq!(&metadata_call, metadata); + Ok(()) +} diff --git a/testing/integration-tests/src/client/mod.rs b/testing/integration-tests/src/client/mod.rs index 0214c4901f..ad18d439e4 100644 --- a/testing/integration-tests/src/client/mod.rs +++ b/testing/integration-tests/src/client/mod.rs @@ -11,6 +11,11 @@ use crate::{ wait_for_blocks, }, }; +use codec::{ + Compact, + Decode, +}; +use frame_metadata::RuntimeMetadataPrefixed; use sp_core::{ sr25519::Pair as Sr25519Pair, storage::well_known_keys, @@ -250,3 +255,29 @@ async fn unsigned_extrinsic_is_same_shape_as_polkadotjs() { // Make sure our encoding is the same as the encoding polkadot UI created. assert_eq!(actual_tx_bytes, expected_tx_bytes); } + +#[tokio::test] +async fn rpc_state_call() { + let ctx = test_context().await; + let api = ctx.client(); + + // Call into the runtime of the chain to get the Metadata. + let metadata_bytes = api + .rpc() + .state_call("Metadata_metadata", None, None) + .await + .unwrap(); + + let cursor = &mut &*metadata_bytes; + let _ = >::decode(cursor).unwrap(); + let meta: RuntimeMetadataPrefixed = Decode::decode(cursor).unwrap(); + let metadata_call = match meta.1 { + frame_metadata::RuntimeMetadata::V14(metadata) => metadata, + _ => panic!("Metadata V14 unavailable"), + }; + + // Compare the runtime API call against the `state_getMetadata`. + let metadata = api.rpc().metadata(None).await.unwrap(); + let metadata = metadata.runtime_metadata(); + assert_eq!(&metadata_call, metadata); +}