mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 18:11:10 +00:00
Introduce Backend trait to allow different RPC (or other) backends to be implemented (#1126)
* WIP backend trait * WIP converting higher level stuff to using Backend impl * more implementing new backend trait, mainly storage focused * Get core code compiling with new backend bits * subxt crate checks passing * fix tests * cargo fmt * clippy/fixes * merging and other fixes * fix test * fix lightclient code * Fix some broken doc links * another book link fix * fix broken test when moving default_rpc_client * fix dry_run test * fix more tests; lightclient and wasm * fix wasm tests * fix some doc examples * use next() instead of next_item() * missing next_item() -> next()s * move legacy RPc methods to LegacyRpcMethods type to host generic param instead of RpcClient * standardise on all RpcClient types prefixed with Rpc, and 'raw' trait types prefixed with RawRpc so it's less ocnfusing which is which * rename fixes * doc fixes * Add back system_dryRun RPC method and rename tx.dry_run() to tx.validate(), to signal that the calls are different * Add a test that we return the correct extrinsic hash from submit() * add TransactionValid details back, and protect against out of range bytes * add test for decoding transaction validation from empty bytes * fix clippy warning
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
// 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 exposes a legacy backend implementation, which relies
|
||||
//! on the legacy RPC API methods.
|
||||
|
||||
pub mod rpc_methods;
|
||||
|
||||
use self::rpc_methods::TransactionStatus as RpcTransactionStatus;
|
||||
use crate::backend::{
|
||||
rpc::RpcClient, Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
|
||||
TransactionStatus,
|
||||
};
|
||||
use crate::{config::Header, Config, Error};
|
||||
use async_trait::async_trait;
|
||||
use futures::{future, future::Either, stream, Future, FutureExt, Stream, StreamExt};
|
||||
use std::collections::VecDeque;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
pub use rpc_methods::LegacyRpcMethods;
|
||||
|
||||
/// The legacy backend.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LegacyBackend<T> {
|
||||
methods: LegacyRpcMethods<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> LegacyBackend<T> {
|
||||
/// Instantiate a new backend which uses the legacy API methods.
|
||||
pub fn new(client: RpcClient) -> Self {
|
||||
Self {
|
||||
methods: LegacyRpcMethods::new(client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> super::sealed::Sealed for LegacyBackend<T> {}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<StorageResponse>, Error> {
|
||||
let methods = self.methods.clone();
|
||||
|
||||
// For each key, return it + a future to get the result.
|
||||
let iter = keys.into_iter().map(move |key| {
|
||||
let methods = methods.clone();
|
||||
async move {
|
||||
let res = methods.state_get_storage(&key, Some(at)).await?;
|
||||
Ok(res.map(|value| StorageResponse { key, value }))
|
||||
}
|
||||
});
|
||||
|
||||
let s = stream::iter(iter)
|
||||
// Resolve the future
|
||||
.then(|fut| fut)
|
||||
// Filter any Options out (ie if we didn't find a value at some key we return nothing for it).
|
||||
.filter_map(|r| future::ready(r.transpose()));
|
||||
|
||||
Ok(StreamOf(Box::pin(s)))
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_keys(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
starting_at: Option<Vec<u8>>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, Error> {
|
||||
Ok(StreamOf(Box::pin(StorageFetchDescendantKeysStream {
|
||||
at,
|
||||
key,
|
||||
methods: self.methods.clone(),
|
||||
done: Default::default(),
|
||||
keys: Default::default(),
|
||||
keys_fut: Default::default(),
|
||||
pagination_start_key: starting_at,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_values(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<StorageResponse>, Error> {
|
||||
let keys_stream = StorageFetchDescendantKeysStream {
|
||||
at,
|
||||
key,
|
||||
methods: self.methods.clone(),
|
||||
done: Default::default(),
|
||||
keys: Default::default(),
|
||||
keys_fut: Default::default(),
|
||||
pagination_start_key: Default::default(),
|
||||
};
|
||||
|
||||
Ok(StreamOf(Box::pin(StorageFetchDescendantValuesStream {
|
||||
keys: keys_stream,
|
||||
next_key: None,
|
||||
value_fut: Default::default(),
|
||||
})))
|
||||
}
|
||||
|
||||
async fn genesis_hash(&self) -> Result<T::Hash, Error> {
|
||||
self.methods.genesis_hash().await
|
||||
}
|
||||
|
||||
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error> {
|
||||
self.methods.chain_get_header(Some(at)).await
|
||||
}
|
||||
|
||||
async fn block_body(&self, at: T::Hash) -> Result<Option<Vec<Vec<u8>>>, Error> {
|
||||
let Some(details) = self.methods.chain_get_block(Some(at)).await? else {
|
||||
return Ok(None)
|
||||
};
|
||||
Ok(Some(
|
||||
details.block.extrinsics.into_iter().map(|b| b.0).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<T::Hash>, Error> {
|
||||
let hash = self.methods.chain_get_finalized_head().await?;
|
||||
Ok(BlockRef::from_hash(hash))
|
||||
}
|
||||
|
||||
async fn latest_best_block_ref(&self) -> Result<BlockRef<T::Hash>, Error> {
|
||||
let hash = self
|
||||
.methods
|
||||
.chain_get_block_hash(None)
|
||||
.await?
|
||||
.ok_or_else(|| Error::Other("Latest best block doesn't exist".into()))?;
|
||||
Ok(BlockRef::from_hash(hash))
|
||||
}
|
||||
|
||||
async fn current_runtime_version(&self) -> Result<RuntimeVersion, Error> {
|
||||
let details = self.methods.state_get_runtime_version(None).await?;
|
||||
Ok(RuntimeVersion {
|
||||
spec_version: details.spec_version,
|
||||
transaction_version: details.transaction_version,
|
||||
})
|
||||
}
|
||||
|
||||
async fn stream_runtime_version(&self) -> Result<StreamOfResults<RuntimeVersion>, Error> {
|
||||
let sub = self.methods.state_subscribe_runtime_version().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|v| RuntimeVersion {
|
||||
spec_version: v.spec_version,
|
||||
transaction_version: v.transaction_version,
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
}
|
||||
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
let sub = self.methods.chain_subscribe_all_heads().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
}
|
||||
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
let sub = self.methods.chain_subscribe_new_heads().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
}
|
||||
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
let sub: super::rpc::RpcSubscription<<T as Config>::Header> =
|
||||
self.methods.chain_subscribe_finalized_heads().await?;
|
||||
|
||||
// Get the last finalized block immediately so that the stream will emit every finalized block after this.
|
||||
let last_finalized_block_ref = self.latest_finalized_block_ref().await?;
|
||||
let last_finalized_block_num = self
|
||||
.block_header(last_finalized_block_ref.hash())
|
||||
.await?
|
||||
.map(|h| h.number().into());
|
||||
|
||||
// Fill in any missing blocks, because the backend may not emit every finalized block; just the latest ones which
|
||||
// are finalized each time.
|
||||
let sub = subscribe_to_block_headers_filling_in_gaps(
|
||||
self.methods.clone(),
|
||||
sub,
|
||||
last_finalized_block_num,
|
||||
);
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
}
|
||||
|
||||
async fn submit_transaction(
|
||||
&self,
|
||||
extrinsic: &[u8],
|
||||
) -> Result<StreamOfResults<TransactionStatus<T::Hash>>, Error> {
|
||||
let sub = self
|
||||
.methods
|
||||
.author_submit_and_watch_extrinsic(extrinsic)
|
||||
.await?;
|
||||
let sub = sub.filter_map(|r| {
|
||||
let mapped = r
|
||||
.map(|tx| {
|
||||
match tx {
|
||||
// We ignore these because they don't map nicely to the new API. They don't signal "end states" so this should be fine.
|
||||
RpcTransactionStatus::Future => None,
|
||||
RpcTransactionStatus::Retracted(_) => None,
|
||||
// These roughly map across:
|
||||
RpcTransactionStatus::Ready => Some(TransactionStatus::Validated),
|
||||
RpcTransactionStatus::Broadcast(peers) => {
|
||||
Some(TransactionStatus::Broadcasted {
|
||||
num_peers: peers.len() as u32,
|
||||
})
|
||||
}
|
||||
RpcTransactionStatus::InBlock(hash) => {
|
||||
Some(TransactionStatus::InBestBlock { hash })
|
||||
}
|
||||
// These 5 mean that the stream will very likely end:
|
||||
RpcTransactionStatus::FinalityTimeout(_) => {
|
||||
Some(TransactionStatus::Invalid {
|
||||
message: "Finality timeout".into(),
|
||||
})
|
||||
}
|
||||
RpcTransactionStatus::Finalized(hash) => {
|
||||
Some(TransactionStatus::InFinalizedBlock { hash })
|
||||
}
|
||||
RpcTransactionStatus::Usurped(_) => Some(TransactionStatus::Invalid {
|
||||
message: "Transaction was usurped by another with the same nonce"
|
||||
.into(),
|
||||
}),
|
||||
RpcTransactionStatus::Dropped => Some(TransactionStatus::Dropped {
|
||||
message: "Transaction was dropped".into(),
|
||||
}),
|
||||
RpcTransactionStatus::Invalid => Some(TransactionStatus::Invalid {
|
||||
message:
|
||||
"Transaction is invalid (eg because of a bad nonce, signature etc)"
|
||||
.into(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.transpose();
|
||||
|
||||
future::ready(mapped)
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
}
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: T::Hash,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
self.methods
|
||||
.state_call(method, call_parameters, Some(at))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: This is exposed for testing but is not considered stable and may change
|
||||
/// without notice in a patch release.
|
||||
#[doc(hidden)]
|
||||
pub fn subscribe_to_block_headers_filling_in_gaps<T, S, E>(
|
||||
methods: LegacyRpcMethods<T>,
|
||||
sub: S,
|
||||
mut last_block_num: Option<u64>,
|
||||
) -> impl Stream<Item = Result<T::Header, Error>> + Send
|
||||
where
|
||||
T: Config,
|
||||
S: Stream<Item = Result<T::Header, E>> + Send,
|
||||
E: Into<Error> + Send + 'static,
|
||||
{
|
||||
sub.flat_map(move |s| {
|
||||
// Get the header, or return a stream containing just the error.
|
||||
let header = match s {
|
||||
Ok(header) => header,
|
||||
Err(e) => return Either::Left(stream::once(async { Err(e.into()) })),
|
||||
};
|
||||
|
||||
// We want all previous details up to, but not including this current block num.
|
||||
let end_block_num = header.number().into();
|
||||
|
||||
// This is one after the last block we returned details for last time.
|
||||
let start_block_num = last_block_num.map(|n| n + 1).unwrap_or(end_block_num);
|
||||
|
||||
// Iterate over all of the previous blocks we need headers for, ignoring the current block
|
||||
// (which we already have the header info for):
|
||||
let methods = methods.clone();
|
||||
let previous_headers = stream::iter(start_block_num..end_block_num)
|
||||
.then(move |n| {
|
||||
let methods = methods.clone();
|
||||
async move {
|
||||
let hash = methods.chain_get_block_hash(Some(n.into())).await?;
|
||||
let header = methods.chain_get_header(hash).await?;
|
||||
Ok::<_, Error>(header)
|
||||
}
|
||||
})
|
||||
.filter_map(|h| async { h.transpose() });
|
||||
|
||||
// On the next iteration, we'll get details starting just after this end block.
|
||||
last_block_num = Some(end_block_num);
|
||||
|
||||
// Return a combination of any previous headers plus the new header.
|
||||
Either::Right(previous_headers.chain(stream::once(async { Ok(header) })))
|
||||
})
|
||||
}
|
||||
|
||||
/// This provides a stream of values given some prefix `key`. It
|
||||
/// internally manages pagination and such.
|
||||
pub struct StorageFetchDescendantKeysStream<T: Config> {
|
||||
methods: LegacyRpcMethods<T>,
|
||||
key: Vec<u8>,
|
||||
at: T::Hash,
|
||||
// What key do we start paginating from? None = from the beginning.
|
||||
pagination_start_key: Option<Vec<u8>>,
|
||||
// Keys, future and cached:
|
||||
keys_fut: Option<Pin<Box<dyn Future<Output = Result<Vec<Vec<u8>>, Error>> + Send + 'static>>>,
|
||||
keys: VecDeque<Vec<u8>>,
|
||||
// Set to true when we're done:
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl<T: Config> std::marker::Unpin for StorageFetchDescendantKeysStream<T> {}
|
||||
|
||||
// How many storage keys to ask for each time.
|
||||
const STORAGE_FETCH_PAGE_SIZE: u32 = 32;
|
||||
|
||||
impl<T: Config> Stream for StorageFetchDescendantKeysStream<T> {
|
||||
type Item = Result<Vec<u8>, Error>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
let mut this = self.as_mut();
|
||||
|
||||
// We're already done.
|
||||
if this.done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
// We have some keys to hand back already, so do that.
|
||||
if let Some(key) = this.keys.pop_front() {
|
||||
return Poll::Ready(Some(Ok(key)));
|
||||
}
|
||||
|
||||
// Else, we don't have any keys, but we have a fut to get more so poll it.
|
||||
if let Some(mut keys_fut) = this.keys_fut.take() {
|
||||
let Poll::Ready(keys) = keys_fut.poll_unpin(cx) else {
|
||||
this.keys_fut = Some(keys_fut);
|
||||
return Poll::Pending
|
||||
};
|
||||
|
||||
match keys {
|
||||
Ok(keys) => {
|
||||
if keys.is_empty() {
|
||||
// No keys left; we're done!
|
||||
this.done = true;
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
// The last key is where we want to paginate from next time.
|
||||
this.pagination_start_key = keys.last().cloned();
|
||||
// Got new keys; loop around to start returning them.
|
||||
this.keys = keys.into_iter().collect();
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
// Error getting keys? Return it.
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Else, we don't have a fut to get keys yet so start one going.
|
||||
let methods = this.methods.clone();
|
||||
let key = this.key.clone();
|
||||
let at = this.at;
|
||||
let pagination_start_key = this.pagination_start_key.take();
|
||||
let keys_fut = async move {
|
||||
methods
|
||||
.state_get_keys_paged(
|
||||
&key,
|
||||
STORAGE_FETCH_PAGE_SIZE,
|
||||
pagination_start_key.as_deref(),
|
||||
Some(at),
|
||||
)
|
||||
.await
|
||||
};
|
||||
this.keys_fut = Some(Box::pin(keys_fut));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This provides a stream of values given some stream of keys.
|
||||
pub struct StorageFetchDescendantValuesStream<T: Config> {
|
||||
// Stream of keys.
|
||||
keys: StorageFetchDescendantKeysStream<T>,
|
||||
next_key: Option<Vec<u8>>,
|
||||
// Then we track the next value:
|
||||
value_fut:
|
||||
Option<Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Error>> + Send + 'static>>>,
|
||||
}
|
||||
|
||||
impl<T: Config> Stream for StorageFetchDescendantValuesStream<T> {
|
||||
type Item = Result<StorageResponse, Error>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut();
|
||||
loop {
|
||||
// If we're waiting on the next value then poll that future:
|
||||
if let Some(mut value_fut) = this.value_fut.take() {
|
||||
match value_fut.poll_unpin(cx) {
|
||||
Poll::Ready(Ok(Some(value))) => {
|
||||
let key = this.next_key.take().expect("key should exist");
|
||||
return Poll::Ready(Some(Ok(StorageResponse { key, value })));
|
||||
}
|
||||
Poll::Ready(Ok(None)) => {
|
||||
// No value back for some key? Skip.
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Some(Err(e))),
|
||||
Poll::Pending => {
|
||||
this.value_fut = Some(value_fut);
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Else, if we have the next key then let's start waiting on the next value.
|
||||
if let Some(key) = &this.next_key {
|
||||
let key = key.clone();
|
||||
let methods = this.keys.methods.clone();
|
||||
let at = this.keys.at;
|
||||
let fut = async move { methods.state_get_storage(&key, Some(at)).await };
|
||||
|
||||
this.value_fut = Some(Box::pin(fut));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Else, poll the keys stream to get the next key.
|
||||
match this.keys.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(key))) => {
|
||||
this.next_key = Some(key);
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Pending => {
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
// 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 raw legacy RPC methods.
|
||||
|
||||
use crate::backend::rpc::{rpc_params, RpcClient, RpcSubscription};
|
||||
use crate::metadata::Metadata;
|
||||
use crate::{Config, Error};
|
||||
use codec::Decode;
|
||||
use primitive_types::U256;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An interface to call the legacy 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 LegacyRpcMethods<T> {
|
||||
client: RpcClient,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for LegacyRpcMethods<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
client: self.client.clone(),
|
||||
_marker: self._marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for LegacyRpcMethods<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LegacyRpcMethods")
|
||||
.field("client", &self.client)
|
||||
.field("_marker", &self._marker)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> LegacyRpcMethods<T> {
|
||||
/// Instantiate the legacy RPC method interface.
|
||||
pub fn new(client: RpcClient) -> Self {
|
||||
LegacyRpcMethods {
|
||||
client,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the raw bytes for a given storage key
|
||||
pub async fn state_get_storage(
|
||||
&self,
|
||||
key: &[u8],
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<StorageKey>, Error> {
|
||||
let params = rpc_params![to_hex(key), hash];
|
||||
let data: Option<Bytes> = self.client.request("state_getStorage", params).await?;
|
||||
Ok(data.map(|b| b.0))
|
||||
}
|
||||
|
||||
/// Returns the keys with prefix with pagination support.
|
||||
/// Up to `count` keys will be returned.
|
||||
/// If `start_key` is passed, return next keys in storage in lexicographic order.
|
||||
pub async fn state_get_keys_paged(
|
||||
&self,
|
||||
key: &[u8],
|
||||
count: u32,
|
||||
start_key: Option<&[u8]>,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageData>, Error> {
|
||||
let start_key = start_key.map(to_hex);
|
||||
let params = rpc_params![to_hex(key), count, start_key, at];
|
||||
let data: Vec<Bytes> = self.client.request("state_getKeysPaged", params).await?;
|
||||
Ok(data.into_iter().map(|b| b.0).collect())
|
||||
}
|
||||
|
||||
/// Fetch the genesis hash
|
||||
pub async fn genesis_hash(&self) -> Result<T::Hash, Error> {
|
||||
let block_zero = 0u32;
|
||||
let params = rpc_params![block_zero];
|
||||
let genesis_hash: Option<T::Hash> =
|
||||
self.client.request("chain_getBlockHash", params).await?;
|
||||
genesis_hash.ok_or_else(|| "Genesis hash not found".into())
|
||||
}
|
||||
|
||||
/// Fetch the metadata via the legacy `state_getMetadata` RPC method.
|
||||
pub async fn state_get_metadata(&self, at: Option<T::Hash>) -> Result<Metadata, Error> {
|
||||
let bytes: Bytes = self
|
||||
.client
|
||||
.request("state_getMetadata", rpc_params![at])
|
||||
.await?;
|
||||
let metadata = Metadata::decode(&mut &bytes[..])?;
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Fetch system health
|
||||
pub async fn system_health(&self) -> Result<SystemHealth, Error> {
|
||||
self.client.request("system_health", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system chain
|
||||
pub async fn system_chain(&self) -> Result<String, Error> {
|
||||
self.client.request("system_chain", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system name
|
||||
pub async fn system_name(&self) -> Result<String, Error> {
|
||||
self.client.request("system_name", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system version
|
||||
pub async fn system_version(&self) -> Result<String, Error> {
|
||||
self.client.request("system_version", rpc_params![]).await
|
||||
}
|
||||
|
||||
/// Fetch system properties
|
||||
pub async fn system_properties(&self) -> Result<SystemProperties, Error> {
|
||||
self.client
|
||||
.request("system_properties", rpc_params![])
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get a header
|
||||
pub async fn chain_get_header(
|
||||
&self,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<T::Header>, Error> {
|
||||
let params = rpc_params![hash];
|
||||
let header = self.client.request("chain_getHeader", params).await?;
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Get a block hash, returns hash of latest _best_ block by default.
|
||||
pub async fn chain_get_block_hash(
|
||||
&self,
|
||||
block_number: Option<BlockNumber>,
|
||||
) -> Result<Option<T::Hash>, Error> {
|
||||
let params = rpc_params![block_number];
|
||||
let block_hash = self.client.request("chain_getBlockHash", params).await?;
|
||||
Ok(block_hash)
|
||||
}
|
||||
|
||||
/// Get a block hash of the latest finalized block
|
||||
pub async fn chain_get_finalized_head(&self) -> Result<T::Hash, Error> {
|
||||
let hash = self
|
||||
.client
|
||||
.request("chain_getFinalizedHead", rpc_params![])
|
||||
.await?;
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
/// Get a Block
|
||||
pub async fn chain_get_block(
|
||||
&self,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<BlockDetails<T>>, Error> {
|
||||
let params = rpc_params![hash];
|
||||
let block = self.client.request("chain_getBlock", params).await?;
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// Fetch the runtime version
|
||||
pub async fn state_get_runtime_version(
|
||||
&self,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<RuntimeVersion, Error> {
|
||||
let params = rpc_params![at];
|
||||
let version = self
|
||||
.client
|
||||
.request("state_getRuntimeVersion", params)
|
||||
.await?;
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Subscribe to all new best block headers.
|
||||
pub async fn chain_subscribe_new_heads(&self) -> Result<RpcSubscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
// Despite the name, this returns a stream of all new blocks
|
||||
// imported by the node that happen to be added to the current best chain
|
||||
// (ie all best blocks).
|
||||
"chain_subscribeNewHeads",
|
||||
rpc_params![],
|
||||
"chain_unsubscribeNewHeads",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Subscribe to all new block headers.
|
||||
pub async fn chain_subscribe_all_heads(&self) -> Result<RpcSubscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
// Despite the name, this returns a stream of all new blocks
|
||||
// imported by the node that happen to be added to the current best chain
|
||||
// (ie all best blocks).
|
||||
"chain_subscribeAllHeads",
|
||||
rpc_params![],
|
||||
"chain_unsubscribeAllHeads",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Subscribe to finalized block headers.
|
||||
///
|
||||
/// Note: this may not produce _every_ block in the finalized chain;
|
||||
/// sometimes multiple blocks are finalized at once, and in this case only the
|
||||
/// latest one is returned. the higher level APIs that use this "fill in" the
|
||||
/// gaps for us.
|
||||
pub async fn chain_subscribe_finalized_heads(
|
||||
&self,
|
||||
) -> Result<RpcSubscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
"chain_subscribeFinalizedHeads",
|
||||
rpc_params![],
|
||||
"chain_unsubscribeFinalizedHeads",
|
||||
)
|
||||
.await?;
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Subscribe to runtime version updates that produce changes in the metadata.
|
||||
/// The first item emitted by the stream is the current runtime version.
|
||||
pub async fn state_subscribe_runtime_version(
|
||||
&self,
|
||||
) -> Result<RpcSubscription<RuntimeVersion>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
"state_subscribeRuntimeVersion",
|
||||
rpc_params![],
|
||||
"state_unsubscribeRuntimeVersion",
|
||||
)
|
||||
.await?;
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Create and submit an extrinsic and return corresponding Hash if successful
|
||||
pub async fn author_submit_extrinsic(&self, extrinsic: &[u8]) -> Result<T::Hash, Error> {
|
||||
let params = rpc_params![to_hex(extrinsic)];
|
||||
let xt_hash = self
|
||||
.client
|
||||
.request("author_submitExtrinsic", params)
|
||||
.await?;
|
||||
Ok(xt_hash)
|
||||
}
|
||||
|
||||
/// Create and submit an extrinsic and return a subscription to the events triggered.
|
||||
pub async fn author_submit_and_watch_extrinsic(
|
||||
&self,
|
||||
extrinsic: &[u8],
|
||||
) -> Result<RpcSubscription<TransactionStatus<T::Hash>>, Error> {
|
||||
let params = rpc_params![to_hex(extrinsic)];
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
"author_submitAndWatchExtrinsic",
|
||||
params,
|
||||
"author_unwatchExtrinsic",
|
||||
)
|
||||
.await?;
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Execute a runtime API call via `state_call` RPC method.
|
||||
pub async fn state_call(
|
||||
&self,
|
||||
function: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let call_parameters = call_parameters.unwrap_or_default();
|
||||
let bytes: Bytes = self
|
||||
.client
|
||||
.request(
|
||||
"state_call",
|
||||
rpc_params![function, to_hex(call_parameters), at],
|
||||
)
|
||||
.await?;
|
||||
Ok(bytes.0)
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the dry_run RPC, to test if it would succeed.
|
||||
///
|
||||
/// Returns a [`DryRunResult`], which is the result of performing the dry run.
|
||||
pub async fn dry_run(
|
||||
&self,
|
||||
encoded_signed: &[u8],
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<DryRunResultBytes, Error> {
|
||||
let params = rpc_params![to_hex(encoded_signed), at];
|
||||
let result_bytes: Bytes = self.client.request("system_dryRun", params).await?;
|
||||
Ok(DryRunResultBytes(result_bytes.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage key.
|
||||
pub type StorageKey = Vec<u8>;
|
||||
|
||||
/// Storage data.
|
||||
pub type StorageData = Vec<u8>;
|
||||
|
||||
/// Health struct returned by the RPC
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemHealth {
|
||||
/// Number of connected peers
|
||||
pub peers: usize,
|
||||
/// Is the node syncing
|
||||
pub is_syncing: bool,
|
||||
/// Should this node have any peers
|
||||
///
|
||||
/// Might be false for local chains or when running without discovery.
|
||||
pub should_have_peers: bool,
|
||||
}
|
||||
|
||||
/// System properties; an arbitrary JSON object.
|
||||
pub type SystemProperties = serde_json::Map<String, serde_json::Value>;
|
||||
|
||||
/// A block number
|
||||
pub type BlockNumber = NumberOrHex;
|
||||
|
||||
/// The response from `chain_getBlock`
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(bound = "T: Config")]
|
||||
pub struct BlockDetails<T: Config> {
|
||||
/// The block itself.
|
||||
pub block: Block<T>,
|
||||
/// Block justification.
|
||||
pub justifications: Option<Vec<BlockJustification>>,
|
||||
}
|
||||
|
||||
/// Block details in the [`BlockDetails`].
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Block<T: Config> {
|
||||
/// The block header.
|
||||
pub header: T::Header,
|
||||
/// The accompanying extrinsics.
|
||||
pub extrinsics: Vec<Bytes>,
|
||||
}
|
||||
|
||||
/// An abstraction over justification for a block's validity under a consensus algorithm.
|
||||
pub type BlockJustification = (ConsensusEngineId, EncodedJustification);
|
||||
/// Consensus engine unique ID.
|
||||
pub type ConsensusEngineId = [u8; 4];
|
||||
/// The encoded justification specific to a consensus engine.
|
||||
pub type EncodedJustification = Vec<u8>;
|
||||
|
||||
/// This contains the runtime version information necessary to make transactions, as obtained from
|
||||
/// the RPC call `state_getRuntimeVersion`,
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeVersion {
|
||||
/// Version of the runtime specification. A full-node will not attempt to use its native
|
||||
/// runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`,
|
||||
/// `spec_version` and `authoring_version` are the same between Wasm and native.
|
||||
pub spec_version: u32,
|
||||
|
||||
/// All existing dispatches are fully compatible when this number doesn't change. If this
|
||||
/// number changes, then `spec_version` must change, also.
|
||||
///
|
||||
/// This number must change when an existing dispatchable (module ID, dispatch ID) is changed,
|
||||
/// either through an alteration in its user-level semantics, a parameter
|
||||
/// added/removed/changed, a dispatchable being removed, a module being removed, or a
|
||||
/// dispatchable/module changing its index.
|
||||
///
|
||||
/// It need *not* change when a new module is added or when a dispatchable is added.
|
||||
pub transaction_version: u32,
|
||||
|
||||
/// Fields unnecessary to Subxt are written out to this map.
|
||||
#[serde(flatten)]
|
||||
pub other: std::collections::HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Possible transaction status events.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is copied from `sp-transaction-pool` to avoid a dependency on that crate. Therefore it
|
||||
/// must be kept compatible with that type from the target substrate version.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TransactionStatus<Hash> {
|
||||
/// Transaction is part of the future queue.
|
||||
Future,
|
||||
/// Transaction is part of the ready queue.
|
||||
Ready,
|
||||
/// The transaction has been broadcast to the given peers.
|
||||
Broadcast(Vec<String>),
|
||||
/// Transaction has been included in block with given hash.
|
||||
InBlock(Hash),
|
||||
/// The block this transaction was included in has been retracted.
|
||||
Retracted(Hash),
|
||||
/// Maximum number of finality watchers has been reached,
|
||||
/// old watchers are being removed.
|
||||
FinalityTimeout(Hash),
|
||||
/// Transaction has been finalized by a finality-gadget, e.g GRANDPA
|
||||
Finalized(Hash),
|
||||
/// Transaction has been replaced in the pool, by another transaction
|
||||
/// that provides the same tags. (e.g. same (sender, nonce)).
|
||||
Usurped(Hash),
|
||||
/// Transaction has been dropped from the pool because of the limit.
|
||||
Dropped,
|
||||
/// Transaction is no longer valid in the current state.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Hex-serialized shim for `Vec<u8>`.
|
||||
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord, Debug)]
|
||||
pub struct Bytes(#[serde(with = "impl_serde::serialize")] pub Vec<u8>);
|
||||
impl std::ops::Deref for Bytes {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
&self.0[..]
|
||||
}
|
||||
}
|
||||
impl From<Vec<u8>> for Bytes {
|
||||
fn from(s: Vec<u8>) -> Self {
|
||||
Bytes(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// The decoded result returned from calling `system_dryRun` on some extrinsic.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum DryRunResult {
|
||||
/// The transaction could be included in the block and executed.
|
||||
Success,
|
||||
/// The transaction could be included in the block, but the call failed to dispatch.
|
||||
DispatchError(crate::error::DispatchError),
|
||||
/// The transaction could not be included in the block.
|
||||
TransactionValidityError,
|
||||
}
|
||||
|
||||
/// The bytes representing an error dry running an extrinsic. call [`DryRunResultBytes::into_dry_run_result`]
|
||||
/// to attempt to decode this into something more meaningful.
|
||||
pub struct DryRunResultBytes(pub Vec<u8>);
|
||||
|
||||
impl DryRunResultBytes {
|
||||
/// Attempt to decode the error bytes into a [`DryRunResult`] using the provided [`Metadata`].
|
||||
pub fn into_dry_run_result(
|
||||
self,
|
||||
metadata: &crate::metadata::Metadata,
|
||||
) -> Result<DryRunResult, crate::Error> {
|
||||
// dryRun returns an ApplyExtrinsicResult, which is basically a
|
||||
// `Result<Result<(), DispatchError>, TransactionValidityError>`.
|
||||
let bytes = self.0;
|
||||
if bytes[0] == 0 && bytes[1] == 0 {
|
||||
// Ok(Ok(())); transaction is valid and executed ok
|
||||
Ok(DryRunResult::Success)
|
||||
} else if bytes[0] == 0 && bytes[1] == 1 {
|
||||
// Ok(Err(dispatch_error)); transaction is valid but execution failed
|
||||
let dispatch_error =
|
||||
crate::error::DispatchError::decode_from(&bytes[2..], metadata.clone())?;
|
||||
Ok(DryRunResult::DispatchError(dispatch_error))
|
||||
} else if bytes[0] == 1 {
|
||||
// Err(transaction_error); some transaction validity error (we ignore the details at the moment)
|
||||
Ok(DryRunResult::TransactionValidityError)
|
||||
} else {
|
||||
// unable to decode the bytes; they aren't what we expect.
|
||||
Err(crate::Error::Unknown(bytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A number type that can be serialized both as a number or a string that encodes a number in a
|
||||
/// string.
|
||||
///
|
||||
/// We allow two representations of the block number as input. Either we deserialize to the type
|
||||
/// that is specified in the block type or we attempt to parse given hex value.
|
||||
///
|
||||
/// The primary motivation for having this type is to avoid overflows when using big integers in
|
||||
/// JavaScript (which we consider as an important RPC API consumer).
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum NumberOrHex {
|
||||
/// The number represented directly.
|
||||
Number(u64),
|
||||
/// Hex representation of the number.
|
||||
Hex(U256),
|
||||
}
|
||||
|
||||
impl NumberOrHex {
|
||||
/// Converts this number into an U256.
|
||||
pub fn into_u256(self) -> U256 {
|
||||
match self {
|
||||
NumberOrHex::Number(n) => n.into(),
|
||||
NumberOrHex::Hex(h) => h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberOrHex> for U256 {
|
||||
fn from(num_or_hex: NumberOrHex) -> U256 {
|
||||
num_or_hex.into_u256()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! into_number_or_hex {
|
||||
($($t: ty)+) => {
|
||||
$(
|
||||
impl From<$t> for NumberOrHex {
|
||||
fn from(x: $t) -> Self {
|
||||
NumberOrHex::Number(x.into())
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
into_number_or_hex!(u8 u16 u32 u64);
|
||||
|
||||
impl From<u128> for NumberOrHex {
|
||||
fn from(n: u128) -> Self {
|
||||
NumberOrHex::Hex(n.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<U256> for NumberOrHex {
|
||||
fn from(n: U256) -> Self {
|
||||
NumberOrHex::Hex(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// A quick helper to encode some bytes to hex.
|
||||
fn to_hex(bytes: impl AsRef<[u8]>) -> String {
|
||||
format!("0x{}", hex::encode(bytes.as_ref()))
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
// 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 exposes a backend trait for Subxt which allows us to get and set
|
||||
//! the necessary information (probably from a JSON-RPC API, but that's up to the
|
||||
//! implementation).
|
||||
|
||||
pub mod legacy;
|
||||
pub mod rpc;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::metadata::Metadata;
|
||||
use crate::Config;
|
||||
use async_trait::async_trait;
|
||||
use codec::{Decode, Encode};
|
||||
use futures::{Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Prevent the backend trait being implemented externally.
|
||||
#[doc(hidden)]
|
||||
pub(crate) mod sealed {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
/// This trait exposes the interface that Subxt will use to communicate with
|
||||
/// a backend. Its goal is to be as minimal as possible.
|
||||
#[async_trait]
|
||||
pub trait Backend<T: Config>: sealed::Sealed + Send + Sync + 'static {
|
||||
/// Fetch values from storage.
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<StorageResponse>, Error>;
|
||||
|
||||
/// Fetch keys underneath the given key from storage.
|
||||
async fn storage_fetch_descendant_keys(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
starting_at: Option<Vec<u8>>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, Error>;
|
||||
|
||||
/// Fetch values underneath the given key from storage.
|
||||
async fn storage_fetch_descendant_values(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<StorageResponse>, Error>;
|
||||
|
||||
/// Fetch the genesis hash
|
||||
async fn genesis_hash(&self) -> Result<T::Hash, Error>;
|
||||
|
||||
/// Get a block header
|
||||
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error>;
|
||||
|
||||
/// Return the extrinsics found in the block. Each extrinsic is represented
|
||||
/// by a vector of bytes which has _not_ been SCALE decoded (in other words, the
|
||||
/// first bytes in the vector will decode to the compact encoded length of the extrinsic)
|
||||
async fn block_body(&self, at: T::Hash) -> Result<Option<Vec<Vec<u8>>>, Error>;
|
||||
|
||||
/// Get the most recent finalized block hash.
|
||||
/// Note: needed only in blocks client for finalized block stream; can prolly be removed.
|
||||
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<T::Hash>, Error>;
|
||||
|
||||
/// Get the most recent best block hash.
|
||||
/// Note: needed only in blocks client for finalized block stream; can prolly be removed.
|
||||
async fn latest_best_block_ref(&self) -> Result<BlockRef<T::Hash>, Error>;
|
||||
|
||||
/// Get information about the current runtime.
|
||||
async fn current_runtime_version(&self) -> Result<RuntimeVersion, Error>;
|
||||
|
||||
/// A stream of all new runtime versions as they occur.
|
||||
async fn stream_runtime_version(&self) -> Result<StreamOfResults<RuntimeVersion>, Error>;
|
||||
|
||||
/// A stream of all new block headers as they arrive.
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error>;
|
||||
|
||||
/// A stream of best block headers.
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error>;
|
||||
|
||||
/// A stream of finalized block headers.
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error>;
|
||||
|
||||
/// Submit a transaction. This will return a stream of events about it.
|
||||
async fn submit_transaction(
|
||||
&self,
|
||||
bytes: &[u8],
|
||||
) -> Result<StreamOfResults<TransactionStatus<T::Hash>>, Error>;
|
||||
|
||||
/// Make a call to some runtime API.
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: T::Hash,
|
||||
) -> Result<Vec<u8>, Error>;
|
||||
}
|
||||
|
||||
/// helpeful utility methods derived from those provided on [`Backend`]
|
||||
#[async_trait]
|
||||
pub trait BackendExt<T: Config>: Backend<T> {
|
||||
/// Fetch a single value from storage.
|
||||
async fn storage_fetch_value(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: T::Hash,
|
||||
) -> Result<Option<Vec<u8>>, Error> {
|
||||
self.storage_fetch_values(vec![key], at)
|
||||
.await?
|
||||
.next()
|
||||
.await
|
||||
.transpose()
|
||||
.map(|o| o.map(|s| s.value))
|
||||
}
|
||||
|
||||
/// The same as a [`Backend::call()`], but it will also attempt to decode the
|
||||
/// result into the given type, which is a fairly common operation.
|
||||
async fn call_decoding<D: codec::Decode>(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: T::Hash,
|
||||
) -> Result<D, Error> {
|
||||
let bytes = self.call(method, call_parameters, at).await?;
|
||||
let res = D::decode(&mut &*bytes)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Return the metadata at some version.
|
||||
async fn metadata_at_version(&self, version: u32, at: T::Hash) -> Result<Metadata, Error> {
|
||||
let param = version.encode();
|
||||
|
||||
let opaque: Option<frame_metadata::OpaqueMetadata> = self
|
||||
.call_decoding("Metadata_metadata_at_version", Some(¶m), at)
|
||||
.await?;
|
||||
let Some(opaque) = opaque else {
|
||||
return Err(Error::Other("Metadata version not found".into()));
|
||||
};
|
||||
|
||||
let metadata: Metadata = Decode::decode(&mut &opaque.0[..])?;
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Return V14 metadata from the legacy `Metadata_metadata` call.
|
||||
async fn legacy_metadata(&self, at: T::Hash) -> Result<Metadata, Error> {
|
||||
let opaque: frame_metadata::OpaqueMetadata =
|
||||
self.call_decoding("Metadata_metadata", None, at).await?;
|
||||
let metadata: Metadata = Decode::decode(&mut &opaque.0[..])?;
|
||||
Ok(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B: Backend<T> + ?Sized, T: Config> BackendExt<T> for B {}
|
||||
|
||||
/// An opaque struct which, while alive, indicates that some references to a block
|
||||
/// still exist. This gives the backend the opportunity to keep the corresponding block
|
||||
/// details around for a while if it likes and is able to. No guarantees can be made about
|
||||
/// how long the corresponding details might be available for, but if no references to a block
|
||||
/// exist, then the backend is free to discard any details for it.
|
||||
#[derive(Clone)]
|
||||
pub struct BlockRef<H> {
|
||||
hash: H,
|
||||
// We keep this around so that when it is dropped, it has the
|
||||
// opportunity to tell the backend.
|
||||
_pointer: Option<Arc<dyn BlockRefT>>,
|
||||
}
|
||||
|
||||
impl<H> From<H> for BlockRef<H> {
|
||||
fn from(value: H) -> Self {
|
||||
BlockRef::from_hash(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: PartialEq> PartialEq for BlockRef<H> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.hash == other.hash
|
||||
}
|
||||
}
|
||||
impl<H: Eq> Eq for BlockRef<H> {}
|
||||
|
||||
impl<H: PartialOrd> PartialOrd for BlockRef<H> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.hash.partial_cmp(&other.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Ord> Ord for BlockRef<H> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.hash.cmp(&other.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: std::fmt::Debug> std::fmt::Debug for BlockRef<H> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("BlockRef").field(&self.hash).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: std::hash::Hash> std::hash::Hash for BlockRef<H> {
|
||||
fn hash<Hasher: std::hash::Hasher>(&self, state: &mut Hasher) {
|
||||
self.hash.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> BlockRef<H> {
|
||||
/// A [`BlockRef`] that doesn't reference a given block, but does have an associated hash.
|
||||
/// This is used in the legacy backend, which has no notion of pinning blocks.
|
||||
pub fn from_hash(hash: H) -> Self {
|
||||
Self {
|
||||
hash,
|
||||
_pointer: None,
|
||||
}
|
||||
}
|
||||
/// Construct a [`BlockRef`] from an instance of the underlying trait. It's expected
|
||||
/// that the [`Backend`] implementation will call this if it wants to track which blocks
|
||||
/// are potentially in use.
|
||||
pub fn new<P: BlockRefT>(hash: H, inner: P) -> Self {
|
||||
Self {
|
||||
hash,
|
||||
_pointer: Some(Arc::new(inner)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hash of the referenced block.
|
||||
pub fn hash(&self) -> H
|
||||
where
|
||||
H: Copy,
|
||||
{
|
||||
self.hash
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that a [`Backend`] can implement to know when some block
|
||||
/// can be unpinned: when this is dropped, there are no remaining references
|
||||
/// to the block that it's associated with.
|
||||
pub trait BlockRefT: Send + Sync + 'static {}
|
||||
|
||||
/// A stream of some item.
|
||||
pub struct StreamOf<T>(Pin<Box<dyn Stream<Item = T> + Send + 'static>>);
|
||||
|
||||
impl<T> Stream for StreamOf<T> {
|
||||
type Item = T;
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
self.0.poll_next_unpin(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for StreamOf<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("StreamOf").field(&"<stream>").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StreamOf<T> {
|
||||
/// Construct a new stream.
|
||||
pub fn new(inner: Pin<Box<dyn Stream<Item = T> + Send + 'static>>) -> Self {
|
||||
StreamOf(inner)
|
||||
}
|
||||
|
||||
/// Returns the next item in the stream. This is just a wrapper around
|
||||
/// [`StreamExt::next()`] so that you can avoid the extra import.
|
||||
pub async fn next(&mut self) -> Option<T> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream of [`Result<Item, Error>`].
|
||||
pub type StreamOfResults<T> = StreamOf<Result<T, Error>>;
|
||||
|
||||
/// Runtime version information needed to submit transactions.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RuntimeVersion {
|
||||
/// Version of the runtime specification. A full-node will not attempt to use its native
|
||||
/// runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`,
|
||||
/// `spec_version` and `authoring_version` are the same between Wasm and native.
|
||||
pub spec_version: u32,
|
||||
|
||||
/// All existing dispatches are fully compatible when this number doesn't change. If this
|
||||
/// number changes, then `spec_version` must change, also.
|
||||
///
|
||||
/// This number must change when an existing dispatchable (module ID, dispatch ID) is changed,
|
||||
/// either through an alteration in its user-level semantics, a parameter
|
||||
/// added/removed/changed, a dispatchable being removed, a module being removed, or a
|
||||
/// dispatchable/module changing its index.
|
||||
///
|
||||
/// It need *not* change when a new module is added or when a dispatchable is added.
|
||||
pub transaction_version: u32,
|
||||
}
|
||||
|
||||
/// The status of the transaction.
|
||||
///
|
||||
/// If the status is [`TransactionStatus::InFinalizedBlock`], [`TransactionStatus::Error`],
|
||||
/// [`TransactionStatus::Invalid`] or [`TransactionStatus::Dropped`], then no future
|
||||
/// events will be emitted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TransactionStatus<Hash> {
|
||||
/// 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 hash.
|
||||
InBestBlock {
|
||||
/// Block hash the transaction is in.
|
||||
hash: Hash,
|
||||
},
|
||||
/// Transaction has been finalized by a finality-gadget, e.g GRANDPA
|
||||
InFinalizedBlock {
|
||||
/// Block hash the transaction is in.
|
||||
hash: Hash,
|
||||
},
|
||||
/// Something went wrong in the node.
|
||||
Error {
|
||||
/// Human readable message; what went wrong.
|
||||
message: String,
|
||||
},
|
||||
/// Transaction is invalid (bad nonce, signature etc).
|
||||
Invalid {
|
||||
/// Human readable message; why was it invalid.
|
||||
message: String,
|
||||
},
|
||||
/// The transaction was dropped.
|
||||
Dropped {
|
||||
/// Human readable message; why was it dropped.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A response from calls like [`Backend::storage_fetch_values`] or
|
||||
/// [`Backend::storage_fetch_descendant_values`].
|
||||
pub struct StorageResponse {
|
||||
/// The key.
|
||||
pub key: Vec<u8>,
|
||||
/// The associated value.
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
|
||||
use crate::error::RpcError;
|
||||
use futures::stream::{StreamExt, TryStreamExt};
|
||||
use jsonrpsee::{
|
||||
core::{
|
||||
client::{Client, ClientT, SubscriptionClientT, SubscriptionKind},
|
||||
traits::ToRpcParams,
|
||||
Error as JsonRpseeError,
|
||||
},
|
||||
types::SubscriptionId,
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
struct Params(Option<Box<RawValue>>);
|
||||
|
||||
impl ToRpcParams for Params {
|
||||
fn to_rpc_params(self) -> Result<Option<Box<RawValue>>, JsonRpseeError> {
|
||||
Ok(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClientT for Client {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
Box::pin(async move {
|
||||
let res = ClientT::request(self, method, Params(params))
|
||||
.await
|
||||
.map_err(|e| RpcError::ClientError(Box::new(e)))?;
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
Box::pin(async move {
|
||||
let stream = SubscriptionClientT::subscribe::<Box<RawValue>, _>(
|
||||
self,
|
||||
sub,
|
||||
Params(params),
|
||||
unsub,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| RpcError::ClientError(Box::new(e)))?;
|
||||
|
||||
let id = match stream.kind() {
|
||||
SubscriptionKind::Subscription(SubscriptionId::Str(id)) => {
|
||||
Some(id.clone().into_owned())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let stream = stream
|
||||
.map_err(|e| RpcError::ClientError(Box::new(e)))
|
||||
.boxed();
|
||||
Ok(RawRpcSubscription { stream, id })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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.
|
||||
|
||||
//! RPC types and client for interacting with a substrate node.
|
||||
//!
|
||||
//! These are used behind the scenes by Subxt backend implementations, for
|
||||
//! example [`crate::backend::legacy::LegacyBackend`]. If you need an RPC client,
|
||||
//! then you can manually instantiate one, and then hand it to Subxt if you'd like
|
||||
//! to re-use it for the Subxt connection.
|
||||
//!
|
||||
//! - [`RpcClientT`] is the underlying dynamic RPC implementation. This provides
|
||||
//! the low level [`RpcClientT::request_raw`] and [`RpcClientT::subscribe_raw`]
|
||||
//! methods.
|
||||
//! - [`RpcClient`] is the higher level wrapper around this, offering
|
||||
//! the [`RpcClient::request`] and [`RpcClient::subscribe`] methods.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Fetching the genesis hash.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! use subxt::{
|
||||
//! client::OnlineClient,
|
||||
//! config::SubstrateConfig,
|
||||
//! backend::rpc::RpcClient,
|
||||
//! backend::legacy::LegacyRpcMethods,
|
||||
//! };
|
||||
//!
|
||||
//! // Instantiate a default RPC client pointing at some URL.
|
||||
//! let rpc_client = RpcClient::from_url("ws://localhost:9944")
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! // Instantiate the legacy RPC interface, providing an appropriate
|
||||
//! // config so that it uses the correct types for your chain.
|
||||
//! let rpc_methods = LegacyRpcMethods::<SubstrateConfig>::new(rpc_client.clone());
|
||||
//!
|
||||
//! // Use it to make RPC calls, here using the legacy genesis_hash method.
|
||||
//! let genesis_hash = rpc_methods
|
||||
//! .genesis_hash()
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! println!("{genesis_hash}");
|
||||
//!
|
||||
//! // Instantiate the Subxt interface using the same client and config if you
|
||||
//! // want to reuse the same connection:
|
||||
//! let client = OnlineClient::<SubstrateConfig>::from_rpc_client(rpc_client);
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
// Allow an `rpc.rs` file in the `rpc` folder to align better
|
||||
// with other file names for their types.
|
||||
#![allow(clippy::module_inception)]
|
||||
|
||||
#[cfg(feature = "jsonrpsee")]
|
||||
mod jsonrpsee_impl;
|
||||
|
||||
mod rpc_client;
|
||||
mod rpc_client_t;
|
||||
|
||||
pub use rpc_client_t::{RawRpcFuture, RawRpcSubscription, RawValue, RpcClientT};
|
||||
|
||||
pub use rpc_client::{rpc_params, RpcClient, RpcParams, RpcSubscription};
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::{RawRpcSubscription, RpcClientT};
|
||||
use crate::error::Error;
|
||||
use futures::{Stream, StreamExt};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{pin::Pin, sync::Arc, task::Poll};
|
||||
|
||||
/// A concrete wrapper around an [`RpcClientT`] which provides some higher level helper methods,
|
||||
/// is cheaply cloneable, and can be handed to things like [`crate::client::OnlineClient`] to
|
||||
/// instantiate it.
|
||||
#[derive(Clone)]
|
||||
pub struct RpcClient {
|
||||
client: Arc<dyn RpcClientT>,
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
#[cfg(feature = "jsonrpsee")]
|
||||
/// Create a default RPC client pointed at some URL, currently based on [`jsonrpsee`].
|
||||
pub async fn from_url<U: AsRef<str>>(url: U) -> Result<Self, Error> {
|
||||
let client = jsonrpsee_helpers::client(url.as_ref())
|
||||
.await
|
||||
.map_err(|e| crate::error::RpcError::ClientError(Box::new(e)))?;
|
||||
Ok(Self::new(client))
|
||||
}
|
||||
|
||||
/// Create a new [`RpcClient`] from an arbitrary [`RpcClientT`] implementation.
|
||||
pub fn new<R: RpcClientT>(client: R) -> Self {
|
||||
RpcClient {
|
||||
client: Arc::new(client),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make an RPC request, given a method name and some parameters.
|
||||
///
|
||||
/// See [`RpcParams`] and the [`rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
pub async fn request<Res: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: RpcParams,
|
||||
) -> Result<Res, Error> {
|
||||
let res = self.client.request_raw(method, params.build()).await?;
|
||||
let val = serde_json::from_str(res.get())?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Subscribe to an RPC endpoint, providing the parameters and the method to call to
|
||||
/// unsubscribe from it again.
|
||||
///
|
||||
/// See [`RpcParams`] and the [`rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
pub async fn subscribe<Res: DeserializeOwned>(
|
||||
&self,
|
||||
sub: &str,
|
||||
params: RpcParams,
|
||||
unsub: &str,
|
||||
) -> Result<RpcSubscription<Res>, Error> {
|
||||
let sub = self
|
||||
.client
|
||||
.subscribe_raw(sub, params.build(), unsub)
|
||||
.await?;
|
||||
Ok(RpcSubscription::new(sub))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RpcClient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("RpcClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for RpcClient {
|
||||
type Target = dyn RpcClientT;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.client
|
||||
}
|
||||
}
|
||||
|
||||
/// Create some [`RpcParams`] to pass to our [`RpcClient`]. [`RpcParams`]
|
||||
/// simply enforces that parameters handed to our [`RpcClient`] methods
|
||||
/// are the correct shape.
|
||||
///
|
||||
/// As with the [`serde_json::json!`] macro, this will panic if you provide
|
||||
/// parameters which cannot successfully be serialized to JSON.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use subxt::backend::rpc::{ rpc_params, RpcParams };
|
||||
///
|
||||
/// // If you provide no params you get `None` back
|
||||
/// let params: RpcParams = rpc_params![];
|
||||
/// assert!(params.build().is_none());
|
||||
///
|
||||
/// // If you provide params you get `Some<Box<RawValue>>` back.
|
||||
/// let params: RpcParams = rpc_params![1, true, "foo"];
|
||||
/// assert_eq!(params.build().unwrap().get(), "[1,true,\"foo\"]");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! rpc_params {
|
||||
($($p:expr), *) => {{
|
||||
// May be unused if empty; no params.
|
||||
#[allow(unused_mut)]
|
||||
let mut params = $crate::backend::rpc::RpcParams::new();
|
||||
$(
|
||||
params.push($p).expect("values passed to rpc_params! must be serializable to JSON");
|
||||
)*
|
||||
params
|
||||
}}
|
||||
}
|
||||
pub use rpc_params;
|
||||
|
||||
/// This represents the parameters passed to an [`RpcClient`], and exists to
|
||||
/// enforce that parameters are provided in the correct format.
|
||||
///
|
||||
/// Prefer to use the [`rpc_params!`] macro for simpler creation of these.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use subxt::backend::rpc::RpcParams;
|
||||
///
|
||||
/// let mut params = RpcParams::new();
|
||||
/// params.push(1).unwrap();
|
||||
/// params.push(true).unwrap();
|
||||
/// params.push("foo").unwrap();
|
||||
///
|
||||
/// assert_eq!(params.build().unwrap().get(), "[1,true,\"foo\"]");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RpcParams(Vec<u8>);
|
||||
|
||||
impl RpcParams {
|
||||
/// Create a new empty set of [`RpcParams`].
|
||||
pub fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
/// Push a parameter into our [`RpcParams`]. This serializes it to JSON
|
||||
/// in the process, and so will return an error if this is not possible.
|
||||
pub fn push<P: Serialize>(&mut self, param: P) -> Result<(), Error> {
|
||||
if self.0.is_empty() {
|
||||
self.0.push(b'[');
|
||||
} else {
|
||||
self.0.push(b',')
|
||||
}
|
||||
serde_json::to_writer(&mut self.0, ¶m)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Build a [`RawValue`] from our params, returning `None` if no parameters
|
||||
/// were provided.
|
||||
pub fn build(mut self) -> Option<Box<RawValue>> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.0.push(b']');
|
||||
let s = unsafe { String::from_utf8_unchecked(self.0) };
|
||||
Some(RawValue::from_string(s).expect("Should be valid JSON"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic RPC Subscription. This implements [`Stream`], and so most of
|
||||
/// the functionality you'll need to interact with it comes from the
|
||||
/// [`StreamExt`] extension trait.
|
||||
pub struct RpcSubscription<Res> {
|
||||
inner: RawRpcSubscription,
|
||||
_marker: std::marker::PhantomData<Res>,
|
||||
}
|
||||
|
||||
impl<Res> std::fmt::Debug for RpcSubscription<Res> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RpcSubscription")
|
||||
.field("inner", &"RawRpcSubscription")
|
||||
.field("_marker", &self._marker)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Res> RpcSubscription<Res> {
|
||||
/// Creates a new [`RpcSubscription`].
|
||||
pub fn new(inner: RawRpcSubscription) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain the ID associated with this subscription.
|
||||
pub fn subscription_id(&self) -> Option<&str> {
|
||||
self.inner.id.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Res: DeserializeOwned> RpcSubscription<Res> {
|
||||
/// Returns the next item in the stream. This is just a wrapper around
|
||||
/// [`StreamExt::next()`] so that you can avoid the extra import.
|
||||
pub async fn next(&mut self) -> Option<Result<Res, Error>> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<Res> std::marker::Unpin for RpcSubscription<Res> {}
|
||||
|
||||
impl<Res: DeserializeOwned> Stream for RpcSubscription<Res> {
|
||||
type Item = Result<Res, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let res = futures::ready!(self.inner.stream.poll_next_unpin(cx));
|
||||
|
||||
// Decode the inner RawValue to the type we're expecting and map
|
||||
// any errors to the right shape:
|
||||
let res = res.map(|r| {
|
||||
r.map_err(|e| e.into())
|
||||
.and_then(|raw_val| serde_json::from_str(raw_val.get()).map_err(|e| e.into()))
|
||||
});
|
||||
|
||||
Poll::Ready(res)
|
||||
}
|
||||
}
|
||||
|
||||
// helpers for a jsonrpsee specific RPC client.
|
||||
#[cfg(all(feature = "jsonrpsee", feature = "native"))]
|
||||
mod jsonrpsee_helpers {
|
||||
pub use jsonrpsee::{
|
||||
client_transport::ws::{Receiver, Sender, Url, WsTransportClientBuilder},
|
||||
core::{
|
||||
client::{Client, ClientBuilder},
|
||||
Error,
|
||||
},
|
||||
};
|
||||
|
||||
/// Build WS RPC client from URL
|
||||
pub async fn client(url: &str) -> Result<Client, Error> {
|
||||
let (sender, receiver) = ws_transport(url).await?;
|
||||
Ok(Client::builder()
|
||||
.max_buffer_capacity_per_subscription(4096)
|
||||
.build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
async fn ws_transport(url: &str) -> Result<(Sender, Receiver), Error> {
|
||||
let url = Url::parse(url).map_err(|e| Error::Transport(e.into()))?;
|
||||
WsTransportClientBuilder::default()
|
||||
.build(url)
|
||||
.await
|
||||
.map_err(|e| Error::Transport(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
// helpers for a jsonrpsee specific RPC client.
|
||||
#[cfg(all(feature = "jsonrpsee", feature = "web", target_arch = "wasm32"))]
|
||||
mod jsonrpsee_helpers {
|
||||
pub use jsonrpsee::{
|
||||
client_transport::web,
|
||||
core::{
|
||||
client::{Client, ClientBuilder},
|
||||
Error,
|
||||
},
|
||||
};
|
||||
|
||||
/// Build web RPC client from URL
|
||||
pub async fn client(url: &str) -> Result<Client, Error> {
|
||||
let (sender, receiver) = web::connect(url)
|
||||
.await
|
||||
.map_err(|e| Error::Transport(e.into()))?;
|
||||
Ok(ClientBuilder::default()
|
||||
.max_buffer_capacity_per_subscription(4096)
|
||||
.build_with_wasm(sender, receiver))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::error::RpcError;
|
||||
use futures::Stream;
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
// Re-exporting for simplicity since it's used a bunch in the trait definition.
|
||||
pub use serde_json::value::RawValue;
|
||||
|
||||
/// A trait describing low level JSON-RPC interactions. Implementations of this can be
|
||||
/// used to instantiate a [`super::RpcClient`], which can be passed to [`crate::OnlineClient`]
|
||||
/// or used for lower level RPC calls via eg [`crate::backend::legacy::LegacyRpcMethods`].
|
||||
///
|
||||
/// This is a low level interface whose methods expect an already-serialized set of params,
|
||||
/// and return an owned but still-serialized [`RawValue`], deferring deserialization to
|
||||
/// the caller. This is the case because we want the methods to be object-safe (which prohibits
|
||||
/// generics), and want to avoid any unnecessary allocations in serializing/deserializing
|
||||
/// parameters.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Implementations are free to panic if the `RawValue`'s passed to `request_raw` or
|
||||
/// `subscribe_raw` are not JSON arrays. Internally, we ensure that this is always the case.
|
||||
pub trait RpcClientT: Send + Sync + 'static {
|
||||
/// Make a raw request for which we expect a single response back from. Implementations
|
||||
/// should expect that the params will either be `None`, or be an already-serialized
|
||||
/// JSON array of parameters.
|
||||
///
|
||||
/// See [`super::RpcParams`] and the [`super::rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
///
|
||||
/// Prefer to use the interface provided on [`super::RpcClient`] where possible.
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>>;
|
||||
|
||||
/// Subscribe to some method. Implementations should expect that the params will
|
||||
/// either be `None`, or be an already-serialized JSON array of parameters.
|
||||
///
|
||||
/// See [`super::RpcParams`] and the [`super::rpc_params!`] macro for an example of how to
|
||||
/// construct the parameters.
|
||||
///
|
||||
/// Prefer to use the interface provided on [`super::RpcClient`] where possible.
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription>;
|
||||
}
|
||||
|
||||
/// A boxed future that is returned from the [`RpcClientT`] methods.
|
||||
pub type RawRpcFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, RpcError>> + Send + 'a>>;
|
||||
|
||||
/// The RPC subscription returned from [`RpcClientT`]'s `subscription` method.
|
||||
pub struct RawRpcSubscription {
|
||||
/// The subscription stream.
|
||||
pub stream: Pin<Box<dyn Stream<Item = Result<Box<RawValue>, RpcError>> + Send + 'static>>,
|
||||
/// The ID associated with the subscription.
|
||||
pub id: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user