mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 04:41:02 +00:00
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:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user