Extend the new api.blocks() to be the primary way to subscribe and fetch blocks/extrinsics/events (#691)

* First pass adding functions to get blocks and extrinsics

* cargo fmt and cache block events

* prefix block hash with 0x

* pin streams for better ergonomics and add an example of subscribing to blocks

* remove unused var

* standardise on _all, _best and _finalized for different block header subs

* WIP center subscribing around blocks

* Remove the event filtering/subscribing  stuff

* clippy

* we need tokio, silly clippy

* add extrinsic_index() call

* Update subxt/src/blocks/block_types.rs

Co-authored-by: Andrew Jones <ascjones@gmail.com>

Co-authored-by: Andrew Jones <ascjones@gmail.com>
This commit is contained in:
James Wilson
2022-11-01 16:53:35 +01:00
committed by GitHub
parent 52d4762d13
commit 33a9ec91af
25 changed files with 646 additions and 1279 deletions
+289
View File
@@ -0,0 +1,289 @@
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{
client::{
OfflineClientT,
OnlineClientT,
},
error::{
BlockError,
Error,
},
events,
rpc::ChainBlockResponse,
Config,
};
use derivative::Derivative;
use futures::lock::Mutex as AsyncMutex;
use sp_runtime::traits::{
Hash,
Header,
};
use std::sync::Arc;
/// A representation of a block.
pub struct Block<T: Config, C> {
header: T::Header,
client: C,
// Since we obtain the same events for every extrinsic, let's
// cache them so that we only ever do that once:
cached_events: CachedEvents<T>,
}
// A cache for our events so we don't fetch them more than once when
// iterating over events for extrinsics.
type CachedEvents<T> = Arc<AsyncMutex<Option<events::Events<T>>>>;
impl<T, C> Block<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
pub(crate) fn new(header: T::Header, client: C) -> Self {
Block {
header,
client,
cached_events: Default::default(),
}
}
/// Return the block hash.
pub fn hash(&self) -> T::Hash {
self.header.hash()
}
/// Return the block number.
pub fn number(&self) -> T::BlockNumber {
*self.header().number()
}
/// Return the entire block header.
pub fn header(&self) -> &T::Header {
&self.header
}
}
impl<T, C> Block<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// Return the events associated with the block, fetching them from the node if necessary.
pub async fn events(&self) -> Result<events::Events<T>, Error> {
get_events(&self.client, self.header.hash(), &self.cached_events).await
}
/// Fetch and return the block body.
pub async fn body(&self) -> Result<BlockBody<T, C>, Error> {
let block_hash = self.header.hash();
let block_details = match self.client.rpc().block(Some(block_hash)).await? {
Some(block) => block,
None => return Err(BlockError::block_hash_not_found(block_hash).into()),
};
Ok(BlockBody::new(
self.client.clone(),
block_details,
self.cached_events.clone(),
))
}
}
/// The body of a block.
pub struct BlockBody<T: Config, C> {
details: ChainBlockResponse<T>,
client: C,
cached_events: CachedEvents<T>,
}
impl<T, C> BlockBody<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
pub(crate) fn new(
client: C,
details: ChainBlockResponse<T>,
cached_events: CachedEvents<T>,
) -> Self {
Self {
details,
client,
cached_events,
}
}
/// Returns an iterator over the extrinsics in the block body.
pub fn extrinsics(&self) -> impl Iterator<Item = Extrinsic<'_, T, C>> {
self.details
.block
.extrinsics
.iter()
.enumerate()
.map(|(idx, e)| {
Extrinsic {
index: idx as u32,
bytes: &e.0,
client: self.client.clone(),
block_hash: self.details.block.header.hash(),
cached_events: self.cached_events.clone(),
_marker: std::marker::PhantomData,
}
})
}
}
/// A single extrinsic in a block.
pub struct Extrinsic<'a, T: Config, C> {
index: u32,
bytes: &'a [u8],
client: C,
block_hash: T::Hash,
cached_events: CachedEvents<T>,
_marker: std::marker::PhantomData<T>,
}
impl<'a, T, C> Extrinsic<'a, T, C>
where
T: Config,
C: OfflineClientT<T>,
{
/// The index of the extrinsic in the block.
pub fn index(&self) -> u32 {
self.index
}
/// The bytes of the extrinsic.
pub fn bytes(&self) -> &'a [u8] {
self.bytes
}
}
impl<'a, T, C> Extrinsic<'a, T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// The events associated with the extrinsic.
pub async fn events(&self) -> Result<ExtrinsicEvents<T>, Error> {
let events =
get_events(&self.client, self.block_hash, &self.cached_events).await?;
let ext_hash = T::Hashing::hash_of(&self.bytes);
Ok(ExtrinsicEvents::new(ext_hash, self.index, events))
}
}
/// The events associated with a given extrinsic.
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct ExtrinsicEvents<T: Config> {
// The hash of the extrinsic (handy to expose here because
// this type is returned from TxProgress things in the most
// basic flows, so it's the only place people can access it
// without complicating things for themselves).
ext_hash: T::Hash,
// The index of the extrinsic:
idx: u32,
// All of the events in the block:
events: events::Events<T>,
}
impl<T: Config> ExtrinsicEvents<T> {
pub(crate) fn new(ext_hash: T::Hash, idx: u32, events: events::Events<T>) -> Self {
Self {
ext_hash,
idx,
events,
}
}
/// Return the hash of the block that the extrinsic is in.
pub fn block_hash(&self) -> T::Hash {
self.events.block_hash()
}
/// The index of the extrinsic that these events are produced from.
pub fn extrinsic_index(&self) -> u32 {
self.idx
}
/// Return the hash of the extrinsic.
pub fn extrinsic_hash(&self) -> T::Hash {
self.ext_hash
}
/// Return all of the events in the block that the extrinsic is in.
pub fn all_events_in_block(&self) -> &events::Events<T> {
&self.events
}
/// Iterate over all of the raw events associated with this transaction.
///
/// This works in the same way that [`events::Events::iter()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn iter(&self) -> impl Iterator<Item = Result<events::EventDetails, Error>> + '_ {
self.events.iter().filter(|ev| {
ev.as_ref()
.map(|ev| ev.phase() == events::Phase::ApplyExtrinsic(self.idx))
.unwrap_or(true) // Keep any errors.
})
}
/// Find all of the transaction events matching the event type provided as a generic parameter.
///
/// This works in the same way that [`events::Events::find()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn find<Ev: events::StaticEvent>(
&self,
) -> impl Iterator<Item = Result<Ev, Error>> + '_ {
self.iter().filter_map(|ev| {
ev.and_then(|ev| ev.as_event::<Ev>().map_err(Into::into))
.transpose()
})
}
/// Iterate through the transaction events using metadata to dynamically decode and skip
/// them, and return the first event found which decodes to the provided `Ev` type.
///
/// This works in the same way that [`events::Events::find_first()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn find_first<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, Error> {
self.find::<Ev>().next().transpose()
}
/// Find an event in those associated with this transaction. Returns true if it was found.
///
/// This works in the same way that [`events::Events::has()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn has<Ev: events::StaticEvent>(&self) -> Result<bool, Error> {
Ok(self.find::<Ev>().next().transpose()?.is_some())
}
}
// Return Events from the cache, or fetch from the node if needed.
async fn get_events<C, T>(
client: &C,
block_hash: T::Hash,
cached_events: &AsyncMutex<Option<events::Events<T>>>,
) -> Result<events::Events<T>, Error>
where
T: Config,
C: OnlineClientT<T>,
{
// Acquire lock on the events cache. We either get back our events or we fetch and set them
// before unlocking, so only one fetch call should ever be made. We do this because the
// same events can be shared across all extrinsics in the block.
let lock = cached_events.lock().await;
let events = match &*lock {
Some(events) => events.clone(),
None => {
events::EventsClient::new(client.clone())
.at(Some(block_hash))
.await?
}
};
Ok(events)
}
+122 -44
View File
@@ -2,9 +2,13 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::Block;
use crate::{
client::OnlineClientT,
error::Error,
error::{
BlockError,
Error,
},
utils::PhantomDataSendSync,
Config,
};
@@ -16,7 +20,13 @@ use futures::{
StreamExt,
};
use sp_runtime::traits::Header;
use std::future::Future;
use std::{
future::Future,
pin::Pin,
};
type BlockStream<T> = Pin<Box<dyn Stream<Item = Result<T, Error>> + Send>>;
type BlockStreamRes<T> = Result<BlockStream<T>, Error>;
/// A client for working with blocks.
#[derive(Derivative)]
@@ -41,64 +51,132 @@ where
T: Config,
Client: OnlineClientT<T>,
{
/// Subscribe to new best block headers.
/// Obtain block details given the provided block hash, or the latest block if `None` is
/// provided.
///
/// # Note
/// # Warning
///
/// This does not produce all the blocks from the chain, just the best blocks.
/// The best block is selected by the consensus algorithm.
/// This calls under the hood the `chain_subscribeNewHeads` RPC method, if you need
/// a subscription of all the blocks please use the `chain_subscribeAllHeads` method.
///
/// These blocks haven't necessarily been finalised yet. Prefer
/// [`BlocksClient::subscribe_finalized_headers()`] if that is important.
pub fn subscribe_headers(
/// This call only supports blocks produced since the most recent
/// runtime upgrade. You can attempt to retrieve older blocks,
/// but may run into errors attempting to work with them.
pub fn at(
&self,
) -> impl Future<Output = Result<impl Stream<Item = Result<T::Header, Error>>, Error>>
+ Send
+ 'static {
block_hash: Option<T::Hash>,
) -> impl Future<Output = Result<Block<T, Client>, Error>> + Send + 'static {
let client = self.client.clone();
async move { client.rpc().subscribe_blocks().await }
async move {
// If block hash is not provided, get the hash
// for the latest block and use that.
let block_hash = match block_hash {
Some(hash) => hash,
None => {
client
.rpc()
.block_hash(None)
.await?
.expect("didn't pass a block number; qed")
}
};
let block_header = match client.rpc().header(Some(block_hash)).await? {
Some(header) => header,
None => return Err(BlockError::block_hash_not_found(block_hash).into()),
};
Ok(Block::new(block_header, client))
}
}
/// Subscribe to finalized block headers.
/// Subscribe to all new blocks imported by the node.
///
/// While the Substrate RPC method does not guarantee that all finalized block headers are
/// provided, this function does.
/// ```
pub fn subscribe_finalized_headers(
/// **Note:** You probably want to use [`Self::subscribe_finalized()`] most of
/// the time.
pub fn subscribe_all(
&self,
) -> impl Future<Output = Result<impl Stream<Item = Result<T::Header, Error>>, Error>>
+ Send
+ 'static {
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, Error>> + Send + 'static
where
Client: Send + Sync + 'static,
{
let client = self.client.clone();
async move { subscribe_finalized_headers(client).await }
header_sub_fut_to_block_sub(self.clone(), async move {
let sub = client.rpc().subscribe_all_block_headers().await?;
BlockStreamRes::Ok(Box::pin(sub))
})
}
/// Subscribe to all new blocks imported by the node onto the current best fork.
///
/// **Note:** You probably want to use [`Self::subscribe_finalized()`] most of
/// the time.
pub fn subscribe_best(
&self,
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, Error>> + Send + 'static
where
Client: Send + Sync + 'static,
{
let client = self.client.clone();
header_sub_fut_to_block_sub(self.clone(), async move {
let sub = client.rpc().subscribe_best_block_headers().await?;
BlockStreamRes::Ok(Box::pin(sub))
})
}
/// Subscribe to finalized blocks.
pub fn subscribe_finalized(
&self,
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, Error>> + Send + 'static
where
Client: Send + Sync + 'static,
{
let client = self.client.clone();
header_sub_fut_to_block_sub(self.clone(), async move {
// Fetch the last finalised block details immediately, so that we'll get
// all blocks after this one.
let last_finalized_block_hash = client.rpc().finalized_head().await?;
let last_finalized_block_num = client
.rpc()
.header(Some(last_finalized_block_hash))
.await?
.map(|h| (*h.number()).into());
let sub = client.rpc().subscribe_finalized_block_headers().await?;
// Adjust the subscription stream to fill in any missing blocks.
BlockStreamRes::Ok(
subscribe_to_block_headers_filling_in_gaps(
client,
last_finalized_block_num,
sub,
)
.boxed(),
)
})
}
}
async fn subscribe_finalized_headers<T, Client>(
client: Client,
) -> Result<impl Stream<Item = Result<T::Header, Error>>, Error>
/// Take a promise that will return a subscription to some block headers,
/// and return a subscription to some blocks based on this.
async fn header_sub_fut_to_block_sub<T, Client, S>(
blocks_client: BlocksClient<T, Client>,
sub: S,
) -> Result<BlockStream<Block<T, Client>>, Error>
where
T: Config,
Client: OnlineClientT<T>,
S: Future<Output = Result<BlockStream<T::Header>, Error>> + Send + 'static,
Client: OnlineClientT<T> + Send + Sync + 'static,
{
// Fetch the last finalised block details immediately, so that we'll get
// all blocks after this one.
let last_finalized_block_hash = client.rpc().finalized_head().await?;
let last_finalized_block_num = client
.rpc()
.header(Some(last_finalized_block_hash))
.await?
.map(|h| (*h.number()).into());
let sub = sub.await?.then(move |header| {
let client = blocks_client.client.clone();
async move {
let header = match header {
Ok(header) => header,
Err(e) => return Err(e),
};
let sub = client.rpc().subscribe_finalized_blocks().await?;
// Adjust the subscription stream to fill in any missing blocks.
Ok(
subscribe_to_block_headers_filling_in_gaps(client, last_finalized_block_num, sub)
.boxed(),
)
Ok(Block::new(header, client))
}
});
BlockStreamRes::Ok(Box::pin(sub))
}
/// Note: This is exposed for testing but is not considered stable and may change
+6
View File
@@ -4,8 +4,14 @@
//! This module exposes the necessary functionality for working with events.
mod block_types;
mod blocks_client;
pub use block_types::{
Block,
Extrinsic,
ExtrinsicEvents,
};
pub use blocks_client::{
subscribe_to_block_headers_filling_in_gaps,
BlocksClient,