diff --git a/Cargo.lock b/Cargo.lock index e991a6653a..6dcbe753ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1979,6 +1979,7 @@ dependencies = [ "parity-scale-codec", "regex", "scale-info", + "serde", "sp-core", "substrate-runner", "subxt", diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index c76ce8fd6b..2ca105f354 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -704,10 +704,6 @@ impl<'a> CustomValueMetadata<'a> { } /// Calculates the hash for the CustomValueMetadata. - /// - /// # Panics - /// - /// Panics if `self.type_id` is not registered in the provided type registry pub fn hash(&self) -> [u8; HASH_LEN] { let mut cache = HashMap::new(); get_custom_value_hash(self, &mut cache) diff --git a/subxt/src/backend/legacy/rpc_methods.rs b/subxt/src/backend/legacy/rpc_methods.rs index b3a46e122f..951bf3f585 100644 --- a/subxt/src/backend/legacy/rpc_methods.rs +++ b/subxt/src/backend/legacy/rpc_methods.rs @@ -73,6 +73,35 @@ impl LegacyRpcMethods { Ok(data.into_iter().map(|b| b.0).collect()) } + /// Query historical storage entries + pub async fn state_query_storage( + &self, + keys: impl IntoIterator, + from: T::Hash, + to: Option, + ) -> Result>, Error> { + let keys: Vec = keys.into_iter().map(to_hex).collect(); + let params = rpc_params![keys, from, to]; + self.client + .request("state_queryStorage", params) + .await + .map_err(Into::into) + } + + /// Query historical storage entries + pub async fn state_query_storage_at( + &self, + keys: impl IntoIterator, + at: Option, + ) -> Result>, Error> { + let keys: Vec = keys.into_iter().map(to_hex).collect(); + let params = rpc_params![keys, at]; + self.client + .request("state_queryStorageAt", params) + .await + .map_err(Into::into) + } + /// Fetch the genesis hash pub async fn genesis_hash(&self) -> Result { let block_zero = 0u32; @@ -158,6 +187,32 @@ impl LegacyRpcMethods { Ok(block) } + /// Reexecute the specified `block_hash` and gather statistics while doing so. + /// + /// This function requires the specified block and its parent to be available + /// at the queried node. If either the specified block or the parent is pruned, + /// this function will return `None`. + pub async fn dev_get_block_stats( + &self, + block_hash: T::Hash, + ) -> Result, Error> { + let params = rpc_params![block_hash]; + let stats = self.client.request("dev_getBlockStats", params).await?; + Ok(stats) + } + + /// Get proof of storage entries at a specific block's state. + pub async fn state_get_read_proof( + &self, + keys: impl IntoIterator, + hash: Option, + ) -> Result, Error> { + let keys: Vec = keys.into_iter().map(to_hex).collect(); + let params = rpc_params![keys, hash]; + let proof = self.client.request("state_getReadProof", params).await?; + Ok(proof) + } + /// Fetch the runtime version pub async fn state_get_runtime_version( &self, @@ -268,6 +323,49 @@ impl LegacyRpcMethods { Ok(subscription) } + /// Insert a key into the keystore. + pub async fn author_insert_key( + &self, + key_type: String, + suri: String, + public: Vec, + ) -> Result<(), Error> { + let params = rpc_params![key_type, suri, Bytes(public)]; + self.client.request("author_insertKey", params).await?; + Ok(()) + } + + /// Generate new session keys and returns the corresponding public keys. + pub async fn author_rotate_keys(&self) -> Result, Error> { + let bytes: Bytes = self + .client + .request("author_rotateKeys", rpc_params![]) + .await?; + Ok(bytes.0) + } + + /// Checks if the keystore has private keys for the given session public keys. + /// + /// `session_keys` is the SCALE encoded session keys object from the runtime. + /// + /// Returns `true` if all private keys could be found. + pub async fn author_has_session_keys(&self, session_keys: Vec) -> Result { + let params = rpc_params![Bytes(session_keys)]; + self.client.request("author_hasSessionKeys", params).await + } + + /// Checks if the keystore has private keys for the given public key and key type. + /// + /// Returns `true` if a private key could be found. + pub async fn author_has_key( + &self, + public_key: Vec, + key_type: String, + ) -> Result { + let params = rpc_params![Bytes(public_key), key_type]; + self.client.request("author_hasKey", params).await + } + /// Execute a runtime API call via `state_call` RPC method. pub async fn state_call( &self, @@ -411,21 +509,6 @@ pub enum TransactionStatus { Invalid, } -/// Hex-serialized shim for `Vec`. -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord, Debug)] -pub struct Bytes(#[serde(with = "impl_serde::serialize")] pub Vec); -impl std::ops::Deref for Bytes { - type Target = [u8]; - fn deref(&self) -> &[u8] { - &self.0[..] - } -} -impl From> for Bytes { - fn from(s: Vec) -> Self { - Bytes(s) - } -} - /// The decoded result returned from calling `system_dryRun` on some extrinsic. #[derive(Debug, PartialEq, Eq)] pub enum DryRunResult { @@ -468,6 +551,51 @@ impl DryRunResultBytes { } } +/// Storage change set +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] +#[serde(rename_all = "camelCase")] +pub struct StorageChangeSet { + /// Block hash + pub block: Hash, + /// A list of changes; tuples of storage key and optional storage data. + pub changes: Vec<(Bytes, Option)>, +} + +/// Statistics of a block returned by the `dev_getBlockStats` RPC. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockStats { + /// The length in bytes of the storage proof produced by executing the block. + pub witness_len: u64, + /// The length in bytes of the storage proof after compaction. + pub witness_compact_len: u64, + /// Length of the block in bytes. + /// + /// This information can also be acquired by downloading the whole block. This merely + /// saves some complexity on the client side. + pub block_len: u64, + /// Number of extrinsics in the block. + /// + /// This information can also be acquired by downloading the whole block. This merely + /// saves some complexity on the client side. + pub num_extrinsics: u64, +} + +/// ReadProof struct returned by the RPC +/// +/// # Note +/// +/// This is copied from `sc-rpc-api` to avoid a dependency on that crate. Therefore it +/// must be kept compatible with that type from the target substrate version. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadProof { + /// Block hash used to generate the proof + pub at: Hash, + /// A proof used to prove that storage entries are included in the storage trie + pub proof: Vec, +} + /// A number type that can be serialized both as a number or a string that encodes a number in a /// string. /// @@ -530,3 +658,18 @@ impl From for NumberOrHex { fn to_hex(bytes: impl AsRef<[u8]>) -> String { format!("0x{}", hex::encode(bytes.as_ref())) } + +/// Hex-serialized shim for `Vec`. +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord, Debug)] +pub struct Bytes(#[serde(with = "impl_serde::serialize")] pub Vec); +impl std::ops::Deref for Bytes { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.0[..] + } +} +impl From> for Bytes { + fn from(s: Vec) -> Self { + Bytes(s) + } +} diff --git a/subxt/src/backend/mod.rs b/subxt/src/backend/mod.rs index b5fda66a16..86a1ace450 100644 --- a/subxt/src/backend/mod.rs +++ b/subxt/src/backend/mod.rs @@ -8,6 +8,7 @@ pub mod legacy; pub mod rpc; +pub mod unstable; use crate::error::Error; use crate::metadata::Metadata; diff --git a/subxt/src/backend/unstable/mod.rs b/subxt/src/backend/unstable/mod.rs new file mode 100644 index 0000000000..c29f8b9c5a --- /dev/null +++ b/subxt/src/backend/unstable/mod.rs @@ -0,0 +1,16 @@ +// 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. + +//! This module will expose a backend implementation based on the new APIs +//! described at . See +//! [`rpc_methods`] for the raw API calls. +//! +//! # Warning +//! +//! Everything in this module is **unstable**, meaning that it could change without +//! warning at any time. + +pub mod rpc_methods; + +pub use rpc_methods::UnstableRpcMethods; diff --git a/subxt/src/backend/unstable/rpc_methods.rs b/subxt/src/backend/unstable/rpc_methods.rs new file mode 100644 index 0000000000..965dc1288b --- /dev/null +++ b/subxt/src/backend/unstable/rpc_methods.rs @@ -0,0 +1,929 @@ +// 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. + +//! An interface to call the API methods. See +//! for details of the API +//! methods exposed here. + +use crate::backend::rpc::{rpc_params, RpcClient, RpcSubscription}; +use crate::{Config, Error}; +use futures::{Stream, StreamExt}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::task::Poll; + +/// An interface to call the unstable RPC methods. This interface is instantiated with +/// some `T: Config` trait which determines some of the types that the RPC methods will +/// take or hand back. +pub struct UnstableRpcMethods { + client: RpcClient, + _marker: std::marker::PhantomData, +} + +impl Clone for UnstableRpcMethods { + fn clone(&self) -> Self { + Self { + client: self.client.clone(), + _marker: self._marker, + } + } +} + +impl std::fmt::Debug for UnstableRpcMethods { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UnstableRpcMethods") + .field("client", &self.client) + .field("_marker", &self._marker) + .finish() + } +} + +impl UnstableRpcMethods { + /// Instantiate the legacy RPC method interface. + pub fn new(client: RpcClient) -> Self { + UnstableRpcMethods { + client, + _marker: std::marker::PhantomData, + } + } + + /// Subscribe to `chainHead_unstable_follow` to obtain all reported blocks by the chain. + /// + /// The subscription ID can be used to make queries for the + /// block's body ([`chainhead_unstable_body`](UnstableRpcMethods::chainhead_unstable_follow)), + /// block's header ([`chainhead_unstable_header`](UnstableRpcMethods::chainhead_unstable_header)), + /// block's storage ([`chainhead_unstable_storage`](UnstableRpcMethods::chainhead_unstable_storage)) and submitting + /// runtime API calls at this block ([`chainhead_unstable_call`](UnstableRpcMethods::chainhead_unstable_call)). + /// + /// # Note + /// + /// When the user is no longer interested in a block, the user is responsible + /// for calling the [`chainhead_unstable_unpin`](UnstableRpcMethods::chainhead_unstable_unpin) method. + /// Failure to do so will result in the subscription being stopped by generating the `Stop` event. + pub async fn chainhead_unstable_follow( + &self, + with_runtime: bool, + ) -> Result>, Error> { + let subscription = self + .client + .subscribe( + "chainHead_unstable_follow", + rpc_params![with_runtime], + "chainHead_unstable_unfollow", + ) + .await?; + + Ok(subscription) + } + + /// Resumes a storage fetch started with chainHead_unstable_storage after it has generated an + /// `operationWaitingForContinue` event. + /// + /// Has no effect if the operationId is invalid or refers to an operation that has emitted a + /// `{"event": "operationInaccessible"` event, or if the followSubscription is invalid or stale. + pub async fn chainhead_unstable_continue( + &self, + follow_subscription: &str, + operation_id: &str, + ) -> Result<(), Error> { + self.client + .request( + "chainHead_unstable_continue", + rpc_params![follow_subscription, operation_id], + ) + .await?; + + Ok(()) + } + + /// Stops an operation started with `chainHead_unstable_body`, `chainHead_unstable_call`, or + /// `chainHead_unstable_storage¦. If the operation was still in progress, this interrupts it. + /// If the operation was already finished, this call has no effect. + /// + /// Has no effect if the `followSubscription` is invalid or stale. + pub async fn chainhead_unstable_stop_operation( + &self, + follow_subscription: &str, + operation_id: &str, + ) -> Result<(), Error> { + self.client + .request( + "chainHead_unstable_stopOperation", + rpc_params![follow_subscription, operation_id], + ) + .await?; + + Ok(()) + } + + /// Call the `chainHead_unstable_body` method and return an operation ID to obtain the block's body. + /// + /// The response events are provided on the `chainHead_follow` subscription and identified by + /// the returned operation ID. + /// + /// # Note + /// + /// The subscription ID is obtained from an open subscription created by + /// [`chainhead_unstable_follow`](UnstableRpcMethods::chainhead_unstable_follow). + pub async fn chainhead_unstable_body( + &self, + subscription_id: &str, + hash: T::Hash, + ) -> Result { + let response = self + .client + .request( + "chainHead_unstable_body", + rpc_params![subscription_id, hash], + ) + .await?; + + Ok(response) + } + + /// Get the block's header using the `chainHead_unstable_header` method. + /// + /// # Note + /// + /// The subscription ID is obtained from an open subscription created by + /// [`chainhead_unstable_follow`](UnstableRpcMethods::chainhead_unstable_follow). + pub async fn chainhead_unstable_header( + &self, + subscription_id: &str, + hash: T::Hash, + ) -> Result, Error> { + // header returned as hex encoded SCALE encoded bytes. + let header: Option = self + .client + .request( + "chainHead_unstable_header", + rpc_params![subscription_id, hash], + ) + .await?; + + let header = header + .map(|h| codec::Decode::decode(&mut &*h.0)) + .transpose()?; + Ok(header) + } + + /// Call the `chainhead_unstable_storage` method and return an operation ID to obtain the block's storage. + /// + /// The response events are provided on the `chainHead_follow` subscription and identified by + /// the returned operation ID. + /// + /// # Note + /// + /// The subscription ID is obtained from an open subscription created by + /// [`chainhead_unstable_follow`](UnstableRpcMethods::chainhead_unstable_follow). + pub async fn chainhead_unstable_storage( + &self, + subscription_id: &str, + hash: T::Hash, + items: impl IntoIterator>, + child_key: Option<&[u8]>, + ) -> Result { + let items: Vec> = items + .into_iter() + .map(|item| StorageQuery { + key: to_hex(item.key), + query_type: item.query_type, + }) + .collect(); + + let response = self + .client + .request( + "chainHead_unstable_storage", + rpc_params![subscription_id, hash, items, child_key.map(to_hex)], + ) + .await?; + + Ok(response) + } + + /// Call the `chainhead_unstable_storage` method and return an operation ID to obtain the runtime API result. + /// + /// The response events are provided on the `chainHead_follow` subscription and identified by + /// the returned operation ID. + /// + /// # Note + /// + /// The subscription ID is obtained from an open subscription created by + /// [`chainhead_unstable_follow`](UnstableRpcMethods::chainhead_unstable_follow). + pub async fn chainhead_unstable_call( + &self, + subscription_id: &str, + hash: T::Hash, + function: &str, + call_parameters: &[u8], + ) -> Result { + let response = self + .client + .request( + "chainHead_unstable_call", + rpc_params![subscription_id, hash, function, to_hex(call_parameters)], + ) + .await?; + + Ok(response) + } + + /// Unpin a block reported by the `chainHead_follow` subscription. + /// + /// # Note + /// + /// The subscription ID is obtained from an open subscription created by + /// [`chainhead_unstable_follow`](UnstableRpcMethods::chainhead_unstable_follow). + pub async fn chainhead_unstable_unpin( + &self, + subscription_id: &str, + hash: T::Hash, + ) -> Result<(), Error> { + self.client + .request( + "chainHead_unstable_unpin", + rpc_params![subscription_id, hash], + ) + .await?; + + Ok(()) + } + + /// Return the genesis hash. + pub async fn chainspec_v1_genesis_hash(&self) -> Result { + let hash = self + .client + .request("chainSpec_v1_genesisHash", rpc_params![]) + .await?; + Ok(hash) + } + + /// Return a string containing the human-readable name of the chain. + pub async fn chainspec_v1_chain_name(&self) -> Result { + let hash = self + .client + .request("chainSpec_v1_chainName", rpc_params![]) + .await?; + Ok(hash) + } + + /// Returns the JSON payload found in the chain specification under the key properties. + /// No guarantee is offered about the content of this object, and so it's up to the caller + /// to decide what to deserialize it into. + pub async fn chainspec_v1_properties( + &self, + ) -> Result { + self.client + .request("chainSpec_v1_properties", rpc_params![]) + .await + } + + /// Returns an array of strings indicating the names of all the JSON-RPC functions supported by + /// the JSON-RPC server. + pub async fn rpc_methods(&self) -> Result, Error> { + self.client.request("rpc_methods", rpc_params![]).await + } + + /// Attempt to submit a transaction, returning events about its progress. + pub async fn transaction_unstable_submit_and_watch( + &self, + tx: &[u8], + ) -> Result, Error> { + let sub = self + .client + .subscribe( + "transaction_unstable_submitAndWatch", + rpc_params![to_hex(tx)], + "transaction_unstable_unwatch", + ) + .await?; + + Ok(TransactionSubscription { sub, done: false }) + } +} + +/// This represents events generated by the `follow` method. +/// +/// The block events are generated in the following order: +/// 1. Initialized - generated only once to signal the latest finalized block +/// 2. NewBlock - a new block was added. +/// 3. BestBlockChanged - indicate that the best block is now the one from this event. The block was +/// announced priorly with the `NewBlock` event. +/// 4. Finalized - State the finalized and pruned blocks. +/// +/// The following events are related to operations: +/// - OperationBodyDone: The response of the `chainHead_body` +/// - OperationCallDone: The response of the `chainHead_call` +/// - OperationStorageItems: Items produced by the `chianHead_storage` +/// - OperationWaitingForContinue: Generated after OperationStorageItems and requires the user to +/// call `chainHead_continue` +/// - OperationStorageDone: The `chainHead_storage` method has produced all the results +/// - OperationInaccessible: The server was unable to provide the result, retries might succeed in +/// the future +/// - OperationError: The server encountered an error, retries will not succeed +/// +/// The stop event indicates that the JSON-RPC server was unable to provide a consistent list of +/// the blocks at the head of the chain. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event")] +pub enum FollowEvent { + /// The latest finalized block. + /// + /// This event is generated only once. + Initialized(Initialized), + /// A new non-finalized block was added. + NewBlock(NewBlock), + /// The best block of the chain. + BestBlockChanged(BestBlockChanged), + /// A list of finalized and pruned blocks. + Finalized(Finalized), + /// The response of the `chainHead_body` method. + OperationBodyDone(OperationBodyDone), + /// The response of the `chainHead_call` method. + OperationCallDone(OperationCallDone), + /// Yield one or more items found in the storage. + OperationStorageItems(OperationStorageItems), + /// Ask the user to call `chainHead_continue` to produce more events + /// regarding the operation id. + OperationWaitingForContinue(OperationId), + /// The responses of the `chainHead_storage` method have been produced. + OperationStorageDone(OperationId), + /// The RPC server was unable to provide the response of the following operation id. + /// + /// Repeating the same operation in the future might succeed. + OperationInaccessible(OperationId), + /// The RPC server encountered an error while processing an operation id. + /// + /// Repeating the same operation in the future will not succeed. + OperationError(OperationError), + /// The subscription is dropped and no further events + /// will be generated. + Stop, +} + +/// Contain information about the latest finalized block. +/// +/// # Note +/// +/// This is the first event generated by the `follow` subscription +/// and is submitted only once. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Initialized { + /// The hash of the latest finalized block. + pub finalized_block_hash: Hash, + /// The runtime version of the finalized block. + /// + /// # Note + /// + /// This is present only if the `with_runtime` flag is set for + /// the `follow` subscription. + pub finalized_block_runtime: Option, +} + +/// The runtime event generated if the `follow` subscription +/// has set the `with_runtime` flag. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub enum RuntimeEvent { + /// The runtime version of this block. + Valid(RuntimeVersionEvent), + /// The runtime could not be obtained due to an error. + Invalid(ErrorEvent), +} + +/// The runtime specification of the current block. +/// +/// This event is generated for: +/// - the first announced block by the follow subscription +/// - blocks that suffered a change in runtime compared with their parents +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RuntimeVersionEvent { + /// Details about this runtime. + pub spec: RuntimeSpec, +} + +/// This contains the runtime version information necessary to make transactions, and is obtained from +/// the "initialized" event of `chainHead_follow` if the `withRuntime` flag is set. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RuntimeSpec { + /// Opaque string indicating the name of the chain. + pub spec_name: String, + + /// Opaque string indicating the name of the implementation of the chain. + pub impl_name: String, + + /// Opaque integer. The JSON-RPC client can assume that the Runtime API call to `Metadata_metadata` + /// will always produce the same output as long as the specVersion is the same. + pub spec_version: u32, + + /// Opaque integer. Whenever the runtime code changes in a backwards-compatible way, the implVersion + /// is modified while the specVersion is left untouched. + pub impl_version: u32, + + /// Opaque integer. Necessary when building the bytes of a transaction. Transactions that have been + /// generated with a different `transaction_version` are incompatible. + pub transaction_version: u32, + + /// Object containing a list of "entry point APIs" supported by the runtime. Each key is an opaque string + /// indicating the API, and each value is an integer version number. Before making a runtime call (using + /// chainHead_call), you should make sure that this list contains the entry point API corresponding to the + /// call and with a known version number. + /// + /// **Note:** In Substrate, the keys in the apis field consists of the hexadecimal-encoded 8-bytes blake2 + /// hash of the name of the API. For example, the `TaggedTransactionQueue` API is 0xd2bc9897eed08f15. + #[serde(with = "hashmap_as_tuple_list")] + pub apis: HashMap, +} + +/// The operation could not be processed due to an error. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ErrorEvent { + /// Reason of the error. + pub error: String, +} + +/// Indicate a new non-finalized block. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NewBlock { + /// The hash of the new block. + pub block_hash: Hash, + /// The parent hash of the new block. + pub parent_block_hash: Hash, + /// The runtime version of the new block. + /// + /// # Note + /// + /// This is present only if the `with_runtime` flag is set for + /// the `follow` subscription. + pub new_runtime: Option, +} + +/// Indicate the block hash of the new best block. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BestBlockChanged { + /// The block hash of the new best block. + pub best_block_hash: Hash, +} + +/// Indicate the finalized and pruned block hashes. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Finalized { + /// Block hashes that are finalized. + pub finalized_block_hashes: Vec, + /// Block hashes that are pruned (removed). + pub pruned_block_hashes: Vec, +} + +/// Indicate the operation id of the event. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OperationId { + /// The operation id of the event. + pub operation_id: String, +} + +/// The response of the `chainHead_body` method. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OperationBodyDone { + /// The operation id of the event. + pub operation_id: String, + /// Array of hexadecimal-encoded scale-encoded extrinsics found in the block. + pub value: Vec, +} + +/// The response of the `chainHead_call` method. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OperationCallDone { + /// The operation id of the event. + pub operation_id: String, + /// Hexadecimal-encoded output of the runtime function call. + pub output: String, +} + +/// The response of the `chainHead_call` method. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OperationStorageItems { + /// The operation id of the event. + pub operation_id: String, + /// The resulting items. + pub items: Vec, +} + +/// Indicate a problem during the operation. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OperationError { + /// The operation id of the event. + pub operation_id: String, + /// The reason of the error. + pub error: String, +} + +/// The storage result. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageResult { + /// The hex-encoded key of the result. + pub key: String, + /// The result of the query. + #[serde(flatten)] + pub result: StorageResultType, +} + +/// The type of the storage query. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum StorageResultType { + /// Fetch the value of the provided key. + Value(String), + /// Fetch the hash of the value of the provided key. + Hash(String), + /// Fetch the closest descendant merkle value. + ClosestDescendantMerkleValue(String), +} + +/// The method respose of `chainHead_body`, `chainHead_call` and `chainHead_storage`. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "result")] +pub enum MethodResponse { + /// The method has started. + Started(MethodResponseStarted), + /// The RPC server cannot handle the request at the moment. + LimitReached, +} + +/// The `started` result of a method. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MethodResponseStarted { + /// The operation id of the response. + pub operation_id: String, + /// The number of items from the back of the `chainHead_storage` that have been discarded. + pub discarded_items: Option, +} + +/// The storage item received as parameter. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageQuery { + /// The provided key. + pub key: Key, + /// The type of the storage query. + #[serde(rename = "type")] + pub query_type: StorageQueryType, +} + +/// The type of the storage query. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum StorageQueryType { + /// Fetch the value of the provided key. + Value, + /// Fetch the hash of the value of the provided key. + Hash, + /// Fetch the closest descendant merkle value. + ClosestDescendantMerkleValue, + /// Fetch the values of all descendants of they provided key. + DescendantsValues, + /// Fetch the hashes of the values of all descendants of they provided key. + DescendantsHashes, +} + +/// A subscription which returns transaction status events, stopping +/// when no more events will be sent. +pub struct TransactionSubscription { + sub: RpcSubscription>, + done: bool, +} + +impl TransactionSubscription { + /// Fetch the next item in the stream. + pub async fn next(&mut self) -> Option<::Item> { + StreamExt::next(self).await + } +} + +impl Stream for TransactionSubscription { + type Item = > as Stream>::Item; + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if self.done { + return Poll::Ready(None); + } + + let res = self.sub.poll_next_unpin(cx); + + if let Poll::Ready(Some(Ok(res))) = &res { + if matches!( + res, + TransactionStatus::Dropped { .. } + | TransactionStatus::Error { .. } + | TransactionStatus::Invalid { .. } + | TransactionStatus::Finalized { .. } + ) { + // No more events will occur after these ones. + self.done = true + } + } + + res + } +} + +/// Transaction progress events +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event")] +pub enum TransactionStatus { + /// Transaction is part of the future queue. + Validated, + /// The transaction has been broadcast to other nodes. + Broadcasted { + /// Number of peers it's been broadcast to. + num_peers: u32, + }, + /// Transaction has been included in block with given details. + /// Null is returned if the transaction is no longer in any block + /// of the best chain. + BestChainBlockIncluded { + /// Details of the block it's been seen in. + block: Option>, + }, + /// The transaction is in a block that's been finalized. + Finalized { + /// Details of the block it's been seen in. + block: TransactionBlockDetails, + }, + /// Something went wrong in the node. + Error { + /// Human readable message; what went wrong. + error: String, + }, + /// Transaction is invalid (bad nonce, signature etc). + Invalid { + /// Human readable message; why was it invalid. + error: String, + }, + /// The transaction was dropped. + Dropped { + /// Was the transaction broadcasted to other nodes before being dropped? + broadcasted: bool, + /// Human readable message; why was it dropped. + error: String, + }, +} + +/// Details of a block that a transaction is seen in. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct TransactionBlockDetails { + /// The block hash. + hash: Hash, + /// The index of the transaction in the block. + #[serde(with = "unsigned_number_as_string")] + index: u64, +} + +/// Hex-serialized shim for `Vec`. +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord, Debug)] +pub struct Bytes(#[serde(with = "impl_serde::serialize")] pub Vec); +impl std::ops::Deref for Bytes { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.0[..] + } +} +impl From> for Bytes { + fn from(s: Vec) -> Self { + Bytes(s) + } +} + +fn to_hex(bytes: impl AsRef<[u8]>) -> String { + format!("0x{}", hex::encode(bytes.as_ref())) +} + +/// Attempt to deserialize either a string or integer into an integer. +/// See +pub(crate) mod unsigned_number_as_string { + use serde::de::{Deserializer, Visitor}; + use std::fmt; + + /// Deserialize a number from a string or number. + pub fn deserialize<'de, N: From, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(NumberVisitor(std::marker::PhantomData)) + } + + struct NumberVisitor(std::marker::PhantomData); + + impl<'de, N: From> Visitor<'de> for NumberVisitor { + type Value = N; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an unsigned integer or a string containing one") + } + + fn visit_str(self, v: &str) -> Result { + let n: u64 = v.parse().map_err(serde::de::Error::custom)?; + Ok(n.into()) + } + + fn visit_u64(self, v: u64) -> Result { + Ok(v.into()) + } + } +} + +/// A temporary shim to decode "spec.apis" if it comes back as an array like: +/// +/// ```text +/// [["0xABC", 1], ["0xCDE", 2]] +/// ``` +/// +/// The expected format (which this also supports deserializing from) is: +/// +/// ```text +/// { "0xABC": 1, "0xCDE": 2 } +/// ``` +/// +/// We can delete this when the correct format is being returned. +/// +/// Adapted from +pub(crate) mod hashmap_as_tuple_list { + use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor}; + use std::collections::HashMap; + use std::fmt; + use std::hash::{BuildHasher, Hash}; + use std::marker::PhantomData; + + /// Deserialize a [`HashMap`] from a list of tuples or object + pub fn deserialize<'de, K, V, BH, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + K: Eq + Hash + Deserialize<'de>, + V: Deserialize<'de>, + BH: BuildHasher + Default, + { + deserializer.deserialize_any(HashMapVisitor(PhantomData)) + } + struct HashMapVisitor(PhantomData HashMap>); + + impl<'de, K, V, BH> Visitor<'de> for HashMapVisitor + where + K: Deserialize<'de> + Eq + Hash, + V: Deserialize<'de>, + BH: BuildHasher + Default, + { + type Value = HashMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a list of key-value pairs") + } + + // Work with maps too: + fn visit_map(self, mut m: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut map = + HashMap::with_capacity_and_hasher(m.size_hint().unwrap_or(0), BH::default()); + while let Some((key, value)) = m.next_entry()? { + map.insert(key, value); + } + Ok(map) + } + + // The shim to also work with sequences of tuples. + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut map = + HashMap::with_capacity_and_hasher(seq.size_hint().unwrap_or(0), BH::default()); + while let Some((key, value)) = seq.next_element()? { + map.insert(key, value); + } + Ok(map) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn can_deserialize_apis_from_tuple_or_object() { + let old_response = serde_json::json!({ + "authoringVersion": 10, + "specName": "westend", + "implName": "parity-westend", + "specVersion": 9122, + "implVersion": 0, + "stateVersion": 1, + "transactionVersion": 7, + "apis": [ + ["0xdf6acb689907609b", 3], + ["0x37e397fc7c91f5e4", 1], + ["0x40fe3ad401f8959a", 5], + ["0xd2bc9897eed08f15", 3], + ["0xf78b278be53f454c", 2], + ["0xaf2c0297a23e6d3d", 1], + ["0x49eaaf1b548a0cb0", 1], + ["0x91d5df18b0d2cf58", 1], + ["0xed99c5acb25eedf5", 3], + ["0xcbca25e39f142387", 2], + ["0x687ad44ad37f03c2", 1], + ["0xab3c0572291feb8b", 1], + ["0xbc9d89904f5b923f", 1], + ["0x37c8bb1350a9a2a8", 1] + ] + }); + let old_spec: RuntimeSpec = serde_json::from_value(old_response).unwrap(); + + let new_response = serde_json::json!({ + "specName": "westend", + "implName": "parity-westend", + "specVersion": 9122, + "implVersion": 0, + "transactionVersion": 7, + "apis": { + "0xdf6acb689907609b": 3, + "0x37e397fc7c91f5e4": 1, + "0x40fe3ad401f8959a": 5, + "0xd2bc9897eed08f15": 3, + "0xf78b278be53f454c": 2, + "0xaf2c0297a23e6d3d": 1, + "0x49eaaf1b548a0cb0": 1, + "0x91d5df18b0d2cf58": 1, + "0xed99c5acb25eedf5": 3, + "0xcbca25e39f142387": 2, + "0x687ad44ad37f03c2": 1, + "0xab3c0572291feb8b": 1, + "0xbc9d89904f5b923f": 1, + "0x37c8bb1350a9a2a8": 1 + } + }); + let new_spec: RuntimeSpec = serde_json::from_value(new_response).unwrap(); + + assert_eq!(old_spec, new_spec); + } + + #[test] + fn can_deserialize_from_number_or_string() { + #[derive(Debug, Deserialize)] + struct Foo64 { + #[serde(with = "super::unsigned_number_as_string")] + num: u64, + } + #[derive(Debug, Deserialize)] + struct Foo32 { + #[serde(with = "super::unsigned_number_as_string")] + num: u128, + } + + let from_string = serde_json::json!({ + "num": "123" + }); + let from_num = serde_json::json!({ + "num": 123 + }); + let from_err = serde_json::json!({ + "num": "123a" + }); + + let f1: Foo64 = + serde_json::from_value(from_string.clone()).expect("can deser string into u64"); + let f2: Foo32 = serde_json::from_value(from_string).expect("can deser string into u32"); + let f3: Foo64 = serde_json::from_value(from_num.clone()).expect("can deser num into u64"); + let f4: Foo32 = serde_json::from_value(from_num).expect("can deser num into u32"); + + assert_eq!(f1.num, 123); + assert_eq!(f2.num, 123); + assert_eq!(f3.num, 123); + assert_eq!(f4.num, 123); + + // Invalid things should lead to an error: + let _ = serde_json::from_value::(from_err) + .expect_err("can't deser invalid num into u32"); + } +} diff --git a/subxt/src/config/mod.rs b/subxt/src/config/mod.rs index a3ee59f91a..d6b05cf24a 100644 --- a/subxt/src/config/mod.rs +++ b/subxt/src/config/mod.rs @@ -82,7 +82,7 @@ pub trait Hasher { } /// This represents the block header type used by a node. -pub trait Header: Sized + Encode { +pub trait Header: Sized + Encode + Decode { /// The block number type for this header. type Number: Into; /// The hasher used to hash this header. @@ -105,7 +105,7 @@ mod substrate_impls { impl Header for sp_runtime::generic::Header where - Self: Encode, + Self: Encode + Decode, N: Copy + Into + Into + TryFrom, H: sp_runtime::traits::Hash + Hasher, { diff --git a/subxt/src/config/substrate.rs b/subxt/src/config/substrate.rs index aed2b67660..609b20a778 100644 --- a/subxt/src/config/substrate.rs +++ b/subxt/src/config/substrate.rs @@ -71,7 +71,7 @@ impl Header for SubstrateHeader where N: Copy + Into + Into + TryFrom + Encode, H: Hasher + Encode, - SubstrateHeader: Encode, + SubstrateHeader: Encode + Decode, { type Number = N; type Hasher = H; diff --git a/testing/integration-tests/Cargo.toml b/testing/integration-tests/Cargo.toml index 099b93382e..89f2d5dae3 100644 --- a/testing/integration-tests/Cargo.toml +++ b/testing/integration-tests/Cargo.toml @@ -25,6 +25,7 @@ frame-metadata = { workspace = true } futures = { workspace = true } hex = { workspace = true } regex = { workspace = true } +serde = { workspace = true } scale-info = { workspace = true, features = ["bit-vec"] } sp-core = { workspace = true } syn = { workspace = true } diff --git a/testing/integration-tests/src/full_client/client/mod.rs b/testing/integration-tests/src/full_client/client/mod.rs index d30b4bdb3d..4740dab6ea 100644 --- a/testing/integration-tests/src/full_client/client/mod.rs +++ b/testing/integration-tests/src/full_client/client/mod.rs @@ -16,6 +16,7 @@ use subxt::{ use subxt_signer::sr25519::dev; mod legacy_rpcs; +mod unstable_rpcs; #[tokio::test] async fn storage_fetch_raw_keys() { diff --git a/testing/integration-tests/src/full_client/client/unstable_rpcs.rs b/testing/integration-tests/src/full_client/client/unstable_rpcs.rs new file mode 100644 index 0000000000..e70e14cc34 --- /dev/null +++ b/testing/integration-tests/src/full_client/client/unstable_rpcs.rs @@ -0,0 +1,313 @@ +// 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. + +//! Just sanity checking some of the new RPC methods to try and +//! catch differences as the implementations evolve. + +use crate::{test_context, utils::node_runtime}; +use assert_matches::assert_matches; +use codec::Encode; +use subxt::{ + backend::rpc::RpcSubscription, + backend::unstable::rpc_methods::{ + FollowEvent, Initialized, MethodResponse, RuntimeEvent, RuntimeVersionEvent, StorageQuery, + StorageQueryType, + }, + utils::AccountId32, +}; +use subxt_signer::sr25519::dev; + +#[tokio::test] +async fn chainhead_unstable_follow() { + let ctx = test_context().await; + let rpc = ctx.unstable_rpc_methods().await; + let legacy_rpc = ctx.legacy_rpc_methods().await; + + // Check subscription with runtime updates set on false. + let mut blocks = rpc.chainhead_unstable_follow(false).await.unwrap(); + let event = blocks.next().await.unwrap().unwrap(); + // The initialized event should contain the finalized block hash. + let finalized_block_hash = legacy_rpc.chain_get_finalized_head().await.unwrap(); + assert_eq!( + event, + FollowEvent::Initialized(Initialized { + finalized_block_hash, + finalized_block_runtime: None, + }) + ); + + // Expect subscription to produce runtime versions. + let mut blocks = rpc.chainhead_unstable_follow(true).await.unwrap(); + let event = blocks.next().await.unwrap().unwrap(); + // The initialized event should contain the finalized block hash. + let finalized_block_hash = legacy_rpc.chain_get_finalized_head().await.unwrap(); + let runtime_version = ctx.client().runtime_version(); + + assert_matches!( + event, + FollowEvent::Initialized(init) => { + assert_eq!(init.finalized_block_hash, finalized_block_hash); + if let Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec })) = init.finalized_block_runtime { + assert_eq!(spec.spec_version, runtime_version.spec_version); + assert_eq!(spec.transaction_version, runtime_version.transaction_version); + } else { + panic!("runtime details not provided with init event, got {:?}", init.finalized_block_runtime); + } + } + ); +} + +#[tokio::test] +async fn chainhead_unstable_body() { + let ctx = test_context().await; + let rpc = ctx.unstable_rpc_methods().await; + + let mut blocks = rpc.chainhead_unstable_follow(false).await.unwrap(); + let event = blocks.next().await.unwrap().unwrap(); + let hash = match event { + FollowEvent::Initialized(init) => init.finalized_block_hash, + _ => panic!("Unexpected event"), + }; + let sub_id = blocks.subscription_id().unwrap(); + + // Fetch the block's body. + let response = rpc.chainhead_unstable_body(sub_id, hash).await.unwrap(); + let operation_id = match response { + MethodResponse::Started(started) => started.operation_id, + MethodResponse::LimitReached => panic!("Expected started response"), + }; + + // Response propagated to `chainHead_follow`. + let event = next_operation_event(&mut blocks).await; + assert_matches!( + event, + FollowEvent::OperationBodyDone(done) if done.operation_id == operation_id + ); +} + +#[tokio::test] +async fn chainhead_unstable_header() { + let ctx = test_context().await; + let rpc = ctx.unstable_rpc_methods().await; + let legacy_rpc = ctx.legacy_rpc_methods().await; + + let mut blocks = rpc.chainhead_unstable_follow(false).await.unwrap(); + let event = blocks.next().await.unwrap().unwrap(); + let hash = match event { + FollowEvent::Initialized(init) => init.finalized_block_hash, + _ => panic!("Unexpected event"), + }; + let sub_id = blocks.subscription_id().unwrap(); + + let new_header = legacy_rpc + .chain_get_header(Some(hash)) + .await + .unwrap() + .unwrap(); + let old_header = rpc + .chainhead_unstable_header(sub_id, hash) + .await + .unwrap() + .unwrap(); + + assert_eq!(new_header, old_header); +} + +#[tokio::test] +async fn chainhead_unstable_storage() { + let ctx = test_context().await; + let api = ctx.client(); + let rpc = ctx.unstable_rpc_methods().await; + + let mut blocks = rpc.chainhead_unstable_follow(false).await.unwrap(); + let event = blocks.next().await.unwrap().unwrap(); + let hash = match event { + FollowEvent::Initialized(init) => init.finalized_block_hash, + _ => panic!("Unexpected event"), + }; + let sub_id = blocks.subscription_id().unwrap(); + + let alice: AccountId32 = dev::alice().public_key().into(); + let addr = node_runtime::storage().system().account(alice); + let addr_bytes = api.storage().address_bytes(&addr).unwrap(); + + let items = vec![StorageQuery { + key: addr_bytes.as_slice(), + query_type: StorageQueryType::Value, + }]; + + // Fetch storage. + let response = rpc + .chainhead_unstable_storage(sub_id, hash, items, None) + .await + .unwrap(); + let operation_id = match response { + MethodResponse::Started(started) => started.operation_id, + MethodResponse::LimitReached => panic!("Expected started response"), + }; + + // Response propagated to `chainHead_follow`. + let event = next_operation_event(&mut blocks).await; + assert_matches!( + event, + FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id && + res.items.len() == 1 && + res.items[0].key == format!("0x{}", hex::encode(addr_bytes)) + ); + + let event = next_operation_event(&mut blocks).await; + assert_matches!(event, FollowEvent::OperationStorageDone(res) if res.operation_id == operation_id); +} + +#[tokio::test] +async fn chainhead_unstable_call() { + let ctx = test_context().await; + let rpc = ctx.unstable_rpc_methods().await; + + let mut blocks = rpc.chainhead_unstable_follow(true).await.unwrap(); + let event = blocks.next().await.unwrap().unwrap(); + let hash = match event { + FollowEvent::Initialized(init) => init.finalized_block_hash, + _ => panic!("Unexpected event"), + }; + let sub_id = blocks.subscription_id().unwrap(); + + let alice_id = dev::alice().public_key().to_account_id(); + // Runtime API call. + let response = rpc + .chainhead_unstable_call( + sub_id, + hash, + "AccountNonceApi_account_nonce", + &alice_id.encode(), + ) + .await + .unwrap(); + let operation_id = match response { + MethodResponse::Started(started) => started.operation_id, + MethodResponse::LimitReached => panic!("Expected started response"), + }; + + // Response propagated to `chainHead_follow`. + let event = next_operation_event(&mut blocks).await; + assert_matches!( + event, + FollowEvent::OperationCallDone(res) if res.operation_id == operation_id + ); +} + +#[tokio::test] +async fn chainhead_unstable_unpin() { + let ctx = test_context().await; + let rpc = ctx.unstable_rpc_methods().await; + + let mut blocks = rpc.chainhead_unstable_follow(true).await.unwrap(); + let event = blocks.next().await.unwrap().unwrap(); + let hash = match event { + FollowEvent::Initialized(init) => init.finalized_block_hash, + _ => panic!("Unexpected event"), + }; + let sub_id = blocks.subscription_id().unwrap(); + + assert!(rpc + .chainhead_unstable_unpin(sub_id.clone(), hash) + .await + .is_ok()); + // The block was already unpinned. + assert!(rpc.chainhead_unstable_unpin(sub_id, hash).await.is_err()); +} + +// Ignored until this is implemented in Substrate +#[ignore] +#[tokio::test] +async fn chainspec_v1_genesishash() { + let ctx = test_context().await; + let old_rpc = ctx.legacy_rpc_methods().await; + let rpc = ctx.unstable_rpc_methods().await; + + let a = old_rpc.genesis_hash().await.unwrap(); + let b = rpc.chainspec_v1_genesis_hash().await.unwrap(); + + assert_eq!(a, b); +} + +// Ignored until this is implemented in Substrate +#[ignore] +#[tokio::test] +async fn chainspec_v1_chainname() { + let ctx = test_context().await; + let old_rpc = ctx.legacy_rpc_methods().await; + let rpc = ctx.unstable_rpc_methods().await; + + let a = old_rpc.system_name().await.unwrap(); + let b = rpc.chainspec_v1_chain_name().await.unwrap(); + + assert_eq!(a, b); +} + +// Ignored until this is implemented in Substrate +#[ignore] +#[tokio::test] +async fn chainspec_v1_properties() { + let ctx = test_context().await; + let old_rpc = ctx.legacy_rpc_methods().await; + let rpc = ctx.unstable_rpc_methods().await; + + let a = old_rpc.system_properties().await.unwrap(); + let b = rpc.chainspec_v1_properties().await.unwrap(); + + assert_eq!(a, b); +} + +#[tokio::test] +async fn transaction_unstable_submit_and_watch() { + let ctx = test_context().await; + let rpc = ctx.unstable_rpc_methods().await; + + // Build and sign some random tx, just to get some appropriate bytes: + let payload = node_runtime::tx().system().remark(b"hello".to_vec()); + let tx_bytes = ctx + .client() + .tx() + .create_signed_with_nonce(&payload, &dev::alice(), 0, Default::default()) + .unwrap() + .into_encoded(); + + // Test submitting it: + let mut sub = rpc + .transaction_unstable_submit_and_watch(&tx_bytes) + .await + .unwrap(); + + // Check that the messages we get back on the way to it finishing deserialize ok + // (this will miss some cases). + while let Some(_ev) = sub.next().await.transpose().unwrap() { + // This stream should end when it hits the relevant stopping event. + // If the test continues forever then something isn't working. + // If we hit an error then that's also an issue! + } +} + +/// Ignore block related events and obtain the next event related to an operation. +async fn next_operation_event( + sub: &mut RpcSubscription>, +) -> FollowEvent { + // At most 5 retries. + for _ in 0..5 { + let event = sub.next().await.unwrap().unwrap(); + + match event { + // Can also return the `Stop` event for better debugging. + FollowEvent::Initialized(_) + | FollowEvent::NewBlock(_) + | FollowEvent::BestBlockChanged(_) + | FollowEvent::Finalized(_) => continue, + _ => (), + }; + + return event; + } + + panic!("Cannot find operation related event after 5 produced events"); +} diff --git a/testing/integration-tests/src/lib.rs b/testing/integration-tests/src/lib.rs index 8c13448893..17a9af1f0b 100644 --- a/testing/integration-tests/src/lib.rs +++ b/testing/integration-tests/src/lib.rs @@ -2,8 +2,6 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -#![deny(unused_crate_dependencies)] - #[cfg(test)] pub mod utils; @@ -20,16 +18,6 @@ mod light_client; #[cfg(test)] use test_runtime::node_runtime; -// These dependencies are used for the full client. -#[cfg(all(test, feature = "unstable-light-client"))] -use futures as _; -#[cfg(all(test, not(feature = "unstable-light-client")))] -use regex as _; -#[cfg(all(test, not(feature = "unstable-light-client")))] -use subxt_codegen as _; -#[cfg(all(test, not(feature = "unstable-light-client")))] -use syn as _; - // We don't use this dependency, but it's here so that we // can enable logging easily if need be. Add this to a test // to enable tracing for it: diff --git a/testing/integration-tests/src/light_client/mod.rs b/testing/integration-tests/src/light_client/mod.rs index 6579940dc5..c24c91d8bf 100644 --- a/testing/integration-tests/src/light_client/mod.rs +++ b/testing/integration-tests/src/light_client/mod.rs @@ -35,19 +35,6 @@ use subxt::{ }; use subxt_metadata::Metadata; -// We don't use these dependencies. -use assert_matches as _; -use frame_metadata as _; -use hex as _; -use regex as _; -use scale_info as _; -use sp_core as _; -use subxt_codegen as _; -use subxt_signer as _; -use syn as _; -use tracing as _; -use wabt as _; - type Client = LightClient; // Check that we can subscribe to non-finalized blocks. diff --git a/testing/integration-tests/src/utils/node_proc.rs b/testing/integration-tests/src/utils/node_proc.rs index aa9bdacff2..7bc390818d 100644 --- a/testing/integration-tests/src/utils/node_proc.rs +++ b/testing/integration-tests/src/utils/node_proc.rs @@ -5,7 +5,7 @@ use std::ffi::{OsStr, OsString}; use substrate_runner::SubstrateNode; use subxt::{ - backend::{legacy, rpc}, + backend::{legacy, rpc, unstable}, Config, OnlineClient, }; @@ -36,15 +36,25 @@ where TestNodeProcessBuilder::new(paths) } - /// Hand back an RPC client connected to the test node. + /// Hand back an RPC client connected to the test node which exposes the legacy RPC methods. pub async fn legacy_rpc_methods(&self) -> legacy::LegacyRpcMethods { - let url = format!("ws://127.0.0.1:{}", self.proc.ws_port()); - let rpc_client = rpc::RpcClient::from_url(url) - .await - .expect("Unable to connect RPC client to test node"); + let rpc_client = self.rpc_client().await; legacy::LegacyRpcMethods::new(rpc_client) } + /// Hand back an RPC client connected to the test node which exposes the unstable RPC methods. + pub async fn unstable_rpc_methods(&self) -> unstable::UnstableRpcMethods { + let rpc_client = self.rpc_client().await; + unstable::UnstableRpcMethods::new(rpc_client) + } + + async fn rpc_client(&self) -> rpc::RpcClient { + let url = format!("ws://127.0.0.1:{}", self.proc.ws_port()); + rpc::RpcClient::from_url(url) + .await + .expect("Unable to connect RPC client to test node") + } + /// Returns the subxt client connected to the running node. #[cfg(not(feature = "unstable-light-client"))] pub fn client(&self) -> OnlineClient { diff --git a/testing/ui-tests/src/custom_values.rs b/testing/ui-tests/src/custom_values.rs index 4d6c30579f..a57ee56c6c 100644 --- a/testing/ui-tests/src/custom_values.rs +++ b/testing/ui-tests/src/custom_values.rs @@ -67,7 +67,7 @@ pub fn metadata_custom_values_foo() -> RuntimeMetadataPrefixed { ("Foo".into(), foo_value_metadata.clone()), ("foo".into(), foo_value_metadata.clone()), ("12".into(), foo_value_metadata.clone()), - ("&Hello".into(), foo_value_metadata.clone()), + ("&Hello".into(), foo_value_metadata), ]), }, };