use std::task::Poll;
use crate::{
client::OnlineClientT,
error::{DispatchError, Error, RpcError, TransactionError},
events::EventsClient,
rpc::types::{Subscription, SubstrateTxStatus},
Config,
};
use derivative::Derivative;
use futures::{Stream, StreamExt};
#[derive(Derivative)]
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
pub struct TxProgress<T: Config, C> {
sub: Option<Subscription<SubstrateTxStatus<T::Hash, T::Hash>>>,
ext_hash: T::Hash,
client: C,
}
impl<T: Config, C> Unpin for TxProgress<T, C> {}
impl<T: Config, C> TxProgress<T, C> {
pub fn new(
sub: Subscription<SubstrateTxStatus<T::Hash, T::Hash>>,
client: C,
ext_hash: T::Hash,
) -> Self {
Self {
sub: Some(sub),
client,
ext_hash,
}
}
pub fn extrinsic_hash(&self) -> T::Hash {
self.ext_hash
}
}
impl<T, C> TxProgress<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
pub async fn next_item(&mut self) -> Option<Result<TxStatus<T, C>, Error>> {
self.next().await
}
pub async fn wait_for_in_block(mut self) -> Result<TxInBlock<T, C>, Error> {
while let Some(status) = self.next_item().await {
match status? {
TxStatus::InBlock(s) | TxStatus::Finalized(s) => return Ok(s),
TxStatus::FinalityTimeout(_) => {
return Err(TransactionError::FinalityTimeout.into())
}
TxStatus::Invalid => return Err(TransactionError::Invalid.into()),
TxStatus::Usurped(_) => return Err(TransactionError::Usurped.into()),
TxStatus::Dropped => return Err(TransactionError::Dropped.into()),
_ => continue,
}
}
Err(RpcError::SubscriptionDropped.into())
}
pub async fn wait_for_finalized(mut self) -> Result<TxInBlock<T, C>, Error> {
while let Some(status) = self.next_item().await {
match status? {
TxStatus::Finalized(s) => return Ok(s),
TxStatus::FinalityTimeout(_) => {
return Err(TransactionError::FinalityTimeout.into())
}
TxStatus::Invalid => return Err(TransactionError::Invalid.into()),
TxStatus::Usurped(_) => return Err(TransactionError::Usurped.into()),
TxStatus::Dropped => return Err(TransactionError::Dropped.into()),
_ => continue,
}
}
Err(RpcError::SubscriptionDropped.into())
}
pub async fn wait_for_finalized_success(
self,
) -> Result<crate::blocks::ExtrinsicEvents<T>, Error> {
let evs = self.wait_for_finalized().await?.wait_for_success().await?;
Ok(evs)
}
}
impl<T: Config, C: Clone> Stream for TxProgress<T, C> {
type Item = Result<TxStatus<T, C>, Error>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let sub = match self.sub.as_mut() {
Some(sub) => sub,
None => return Poll::Ready(None),
};
sub.poll_next_unpin(cx).map_ok(|status| {
match status {
SubstrateTxStatus::Future => TxStatus::Future,
SubstrateTxStatus::Ready => TxStatus::Ready,
SubstrateTxStatus::Broadcast(peers) => TxStatus::Broadcast(peers),
SubstrateTxStatus::InBlock(hash) => {
TxStatus::InBlock(TxInBlock::new(hash, self.ext_hash, self.client.clone()))
}
SubstrateTxStatus::Retracted(hash) => TxStatus::Retracted(hash),
SubstrateTxStatus::FinalityTimeout(hash) => {
self.sub = None;
TxStatus::FinalityTimeout(hash)
}
SubstrateTxStatus::Finalized(hash) => {
self.sub = None;
TxStatus::Finalized(TxInBlock::new(hash, self.ext_hash, self.client.clone()))
}
SubstrateTxStatus::Usurped(hash) => {
self.sub = None;
TxStatus::Usurped(hash)
}
SubstrateTxStatus::Dropped => {
self.sub = None;
TxStatus::Dropped
}
SubstrateTxStatus::Invalid => {
self.sub = None;
TxStatus::Invalid
}
}
})
}
}
#[derive(Derivative)]
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
pub enum TxStatus<T: Config, C> {
Future,
Ready,
Broadcast(Vec<String>),
InBlock(TxInBlock<T, C>),
Retracted(T::Hash),
FinalityTimeout(T::Hash),
Finalized(TxInBlock<T, C>),
Usurped(T::Hash),
Dropped,
Invalid,
}
impl<T: Config, C> TxStatus<T, C> {
pub fn as_finalized(&self) -> Option<&TxInBlock<T, C>> {
match self {
Self::Finalized(val) => Some(val),
_ => None,
}
}
pub fn as_in_block(&self) -> Option<&TxInBlock<T, C>> {
match self {
Self::InBlock(val) => Some(val),
_ => None,
}
}
}
#[derive(Derivative)]
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
pub struct TxInBlock<T: Config, C> {
block_hash: T::Hash,
ext_hash: T::Hash,
client: C,
}
impl<T: Config, C> TxInBlock<T, C> {
pub(crate) fn new(block_hash: T::Hash, ext_hash: T::Hash, client: C) -> Self {
Self {
block_hash,
ext_hash,
client,
}
}
pub fn block_hash(&self) -> T::Hash {
self.block_hash
}
pub fn extrinsic_hash(&self) -> T::Hash {
self.ext_hash
}
}
impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
pub async fn wait_for_success(&self) -> Result<crate::blocks::ExtrinsicEvents<T>, Error> {
let events = self.fetch_events().await?;
for ev in events.iter() {
let ev = ev?;
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
let dispatch_error =
DispatchError::decode_from(ev.field_bytes(), self.client.metadata())?;
return Err(dispatch_error.into());
}
}
Ok(events)
}
pub async fn fetch_events(&self) -> Result<crate::blocks::ExtrinsicEvents<T>, Error> {
let block = self
.client
.rpc()
.block(Some(self.block_hash))
.await?
.ok_or(Error::Transaction(TransactionError::BlockNotFound))?;
let extrinsic_idx = block
.block
.extrinsics
.iter()
.position(|ext| {
use crate::config::Hasher;
let hash = T::Hasher::hash_of(&ext.0);
hash == self.ext_hash
})
.ok_or(Error::Transaction(TransactionError::BlockNotFound))?;
let events = EventsClient::new(self.client.clone())
.at(self.block_hash)
.await?;
Ok(crate::blocks::ExtrinsicEvents::new(
self.ext_hash,
extrinsic_idx as u32,
events,
))
}
}
#[cfg(test)]
mod test {
use std::pin::Pin;
use futures::Stream;
use crate::{
client::{OfflineClientT, OnlineClientT},
config::{extrinsic_params::BaseExtrinsicParams, polkadot::PlainTip, WithExtrinsicParams},
error::RpcError,
rpc::{types::SubstrateTxStatus, RpcSubscription, Subscription},
tx::TxProgress,
Config, Error, SubstrateConfig,
};
use serde_json::value::RawValue;
type MockTxProgress = TxProgress<SubstrateConfig, MockClient>;
type MockHash = <WithExtrinsicParams<
SubstrateConfig,
BaseExtrinsicParams<SubstrateConfig, PlainTip>,
> as Config>::Hash;
type MockSubstrateTxStatus = SubstrateTxStatus<MockHash, MockHash>;
#[derive(Clone, Debug)]
struct MockClient;
impl OfflineClientT<SubstrateConfig> for MockClient {
fn metadata(&self) -> crate::Metadata {
panic!("just a mock impl to satisfy trait bounds")
}
fn genesis_hash(&self) -> <SubstrateConfig as crate::Config>::Hash {
panic!("just a mock impl to satisfy trait bounds")
}
fn runtime_version(&self) -> crate::rpc::types::RuntimeVersion {
panic!("just a mock impl to satisfy trait bounds")
}
}
impl OnlineClientT<SubstrateConfig> for MockClient {
fn rpc(&self) -> &crate::rpc::Rpc<SubstrateConfig> {
panic!("just a mock impl to satisfy trait bounds")
}
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_usurped() {
let tx_progress = mock_tx_progress(vec![
SubstrateTxStatus::Ready,
SubstrateTxStatus::Usurped(Default::default()),
]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(Error::Transaction(crate::error::TransactionError::Usurped))
));
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_dropped() {
let tx_progress =
mock_tx_progress(vec![SubstrateTxStatus::Ready, SubstrateTxStatus::Dropped]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(Error::Transaction(crate::error::TransactionError::Dropped))
));
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_invalid() {
let tx_progress =
mock_tx_progress(vec![SubstrateTxStatus::Ready, SubstrateTxStatus::Invalid]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(Error::Transaction(crate::error::TransactionError::Invalid))
));
}
fn mock_tx_progress(statuses: Vec<MockSubstrateTxStatus>) -> MockTxProgress {
let sub = create_substrate_tx_status_subscription(statuses);
TxProgress::new(sub, MockClient, Default::default())
}
fn create_substrate_tx_status_subscription(
elements: Vec<MockSubstrateTxStatus>,
) -> Subscription<MockSubstrateTxStatus> {
let rpc_substription_stream: Pin<
Box<dyn Stream<Item = Result<Box<RawValue>, RpcError>> + Send + 'static>,
> = Box::pin(futures::stream::iter(elements.into_iter().map(|e| {
let s = serde_json::to_string(&e).unwrap();
let r: Box<RawValue> = RawValue::from_string(s).unwrap();
Ok(r)
})));
let rpc_subscription: RpcSubscription = RpcSubscription {
stream: rpc_substription_stream,
id: None,
};
let sub: Subscription<MockSubstrateTxStatus> = Subscription::new(rpc_subscription);
sub
}
}