From 610aacaa0cc6f08030b66f0ca7090458b67cc454 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 9 Dec 2021 12:23:08 +0000 Subject: [PATCH] Implement new API for `sign_and_submit_then_watch` (#354) * WIP Implementing new event subscription API * back to lifetimes, fix example * no more need for accept_weak_inclusion * thread lifetime through to prevent 'temporary dropped' issue * make working with events a little nicer * Get tests compiling * fmt and clippy * _name back to name * dont take ownership, just have stronger note * Attempt to fix test * remove commented-out code * Add a couple more helper methods and a test * Remove custom ExtrinsicFailed handling; treat them like other events * Handle runtime errors in TransactionProgress related bits * cargo fmt + clippy * Fix some of the failing tests * remove unused import * fix transfer_error test * Fix compile errors against new substrate latest * Comment tweaks, and force test-runtime rebuild * Drop the TransactionProgress subscription when we hit 'end' statuses * cargo fmt * find_event to find_first_event and helper to return all matching events * TransactionProgressStatus to TransactionStatus * Copy and improve docs on TransactionStatus from substrate * debug impl for Client to avoid manual debug impls elsewhere * Add and tweak comments, specifically a note about block inclusion on errors * clippy + fmt * Fix docs * Ignore 'error' statuses and adhere to the substrate docs * tweak and improve some comments per @dvdplm's suggestions * Break transaction* structs into separate file * fmt and fix doc link --- codegen/src/api/calls.rs | 2 +- codegen/src/ir.rs | 1 + examples/submit_and_watch.rs | 10 +- src/client.rs | 72 ++--- src/error.rs | 16 + src/events.rs | 66 ++-- src/lib.rs | 8 +- src/rpc.rs | 154 +-------- src/subscription.rs | 69 +--- src/transaction.rs | 449 +++++++++++++++++++++++++++ test-runtime/build.rs | 2 +- tests/integration/frame/balances.rs | 87 ++++-- tests/integration/frame/contracts.rs | 29 +- tests/integration/frame/staking.rs | 56 ++-- tests/integration/frame/sudo.rs | 30 +- tests/integration/frame/system.rs | 20 +- 16 files changed, 691 insertions(+), 380 deletions(-) create mode 100644 src/transaction.rs diff --git a/codegen/src/api/calls.rs b/codegen/src/api/calls.rs index 6d52f597af..f0a0cc898c 100644 --- a/codegen/src/api/calls.rs +++ b/codegen/src/api/calls.rs @@ -68,7 +68,7 @@ pub fn generate_calls( pub fn #fn_name( &self, #( #call_fn_args, )* - ) -> ::subxt::SubmittableExtrinsic { + ) -> ::subxt::SubmittableExtrinsic<'a, T, #call_struct_name> { let call = #call_struct_name { #( #call_args, )* }; ::subxt::SubmittableExtrinsic::new(self.client, call) } diff --git a/codegen/src/ir.rs b/codegen/src/ir.rs index 779fee4545..486d98ceb4 100644 --- a/codegen/src/ir.rs +++ b/codegen/src/ir.rs @@ -72,6 +72,7 @@ impl ItemMod { } } +#[allow(clippy::large_enum_variant)] #[derive(Debug, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum Item { diff --git a/examples/submit_and_watch.rs b/examples/submit_and_watch.rs index 8bc1d8d83a..0bd8c359bb 100644 --- a/examples/submit_and_watch.rs +++ b/examples/submit_and_watch.rs @@ -42,14 +42,20 @@ async fn main() -> Result<(), Box> { .build() .await? .to_runtime_api::>(); - let result = api + + let balance_transfer = api .tx() .balances() .transfer(dest, 10_000) .sign_and_submit_then_watch(&signer) + .await? + .wait_for_finalized_success() .await?; - if let Some(event) = result.find_event::()? { + let transfer_event = + balance_transfer.find_first_event::()?; + + if let Some(event) = transfer_event { println!("Balance transfer success: value: {:?}", event.2); } else { println!("Failed to find Balances::Transfer Event"); diff --git a/src/client.rs b/src/client.rs index b2bd582616..f574454e22 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,10 +15,12 @@ // along with subxt. If not, see . use futures::future; +use sp_runtime::traits::Hash; pub use sp_runtime::traits::SignedExtension; pub use sp_version::RuntimeVersion; use crate::{ + error::Error, events::EventsDecoder, extrinsic::{ self, @@ -27,19 +29,19 @@ use crate::{ UncheckedExtrinsic, }, rpc::{ - ExtrinsicSuccess, Rpc, RpcClient, SystemProperties, }, storage::StorageClient, + transaction::TransactionProgress, AccountData, Call, Config, - Error, ExtrinsicExtraData, Metadata, }; +use std::sync::Arc; /// ClientBuilder for constructing a Client. #[derive(Default)] @@ -47,7 +49,6 @@ pub struct ClientBuilder { url: Option, client: Option, page_size: Option, - accept_weak_inclusion: bool, } impl ClientBuilder { @@ -57,7 +58,6 @@ impl ClientBuilder { url: None, client: None, page_size: None, - accept_weak_inclusion: false, } } @@ -79,12 +79,6 @@ impl ClientBuilder { self } - /// Only check that transactions are InBlock on submit. - pub fn accept_weak_inclusion(mut self) -> Self { - self.accept_weak_inclusion = true; - self - } - /// Creates a new Client. pub async fn build(self) -> Result, Error> { let client = if let Some(client) = self.client { @@ -93,10 +87,7 @@ impl ClientBuilder { let url = self.url.as_deref().unwrap_or("ws://127.0.0.1:9944"); RpcClient::try_from_url(url).await? }; - let mut rpc = Rpc::new(client); - if self.accept_weak_inclusion { - rpc.accept_weak_inclusion(); - } + let rpc = Rpc::new(client); let (metadata, genesis_hash, runtime_version, properties) = future::join4( rpc.metadata(), rpc.genesis_hash(), @@ -111,7 +102,7 @@ impl ClientBuilder { Ok(Client { rpc, genesis_hash: genesis_hash?, - metadata, + metadata: Arc::new(metadata), events_decoder, properties: properties.unwrap_or_else(|_| Default::default()), runtime_version: runtime_version?, @@ -121,28 +112,28 @@ impl ClientBuilder { } /// Client to interface with a substrate node. +#[derive(Clone)] pub struct Client { rpc: Rpc, genesis_hash: T::Hash, - metadata: Metadata, + metadata: Arc, events_decoder: EventsDecoder, properties: SystemProperties, runtime_version: RuntimeVersion, - // _marker: PhantomData<(fn() -> T::Signature, T::Extra)>, iter_page_size: u32, } -impl Clone for Client { - fn clone(&self) -> Self { - Self { - rpc: self.rpc.clone(), - genesis_hash: self.genesis_hash, - metadata: self.metadata.clone(), - events_decoder: self.events_decoder.clone(), - properties: self.properties.clone(), - runtime_version: self.runtime_version.clone(), - iter_page_size: self.iter_page_size, - } +impl std::fmt::Debug for Client { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Client") + .field("rpc", &"") + .field("genesis_hash", &self.genesis_hash) + .field("metadata", &"") + .field("events_decoder", &"") + .field("properties", &self.properties) + .field("runtime_version", &self.runtime_version.to_string()) + .field("iter_page_size", &self.iter_page_size) + .finish() } } @@ -194,37 +185,40 @@ impl Client { } /// A constructed call ready to be signed and submitted. -pub struct SubmittableExtrinsic<'a, T: Config, C> { - client: &'a Client, +pub struct SubmittableExtrinsic<'client, T: Config, C> { + client: &'client Client, call: C, } -impl<'a, T, C> SubmittableExtrinsic<'a, T, C> +impl<'client, T, C> SubmittableExtrinsic<'client, T, C> where T: Config + ExtrinsicExtraData, C: Call + Send + Sync, { /// Create a new [`SubmittableExtrinsic`]. - pub fn new(client: &'a Client, call: C) -> Self { + pub fn new(client: &'client Client, call: C) -> Self { Self { client, call } } /// Creates and signs an extrinsic and submits it to the chain. /// - /// Returns when the extrinsic has successfully been included in the block, together with any - /// events which were triggered by the extrinsic. + /// Returns a [`TransactionProgress`], which can be used to track the status of the transaction + /// and obtain details about it, once it has made it into a block. pub async fn sign_and_submit_then_watch( self, signer: &(dyn Signer + Send + Sync), - ) -> Result, Error> + ) -> Result, Error> where <<>::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync + 'static { + // Sign the call data to create our extrinsic. let extrinsic = self.create_signed(signer, Default::default()).await?; - self.client - .rpc() - .submit_and_watch_extrinsic(extrinsic, self.client.events_decoder()) - .await + // Get a hash of the extrinsic (we'll need this later). + let ext_hash = T::Hashing::hash_of(&extrinsic); + // Submit and watch for transaction progress. + let sub = self.client.rpc().watch_extrinsic(extrinsic).await?; + + Ok(TransactionProgress::new(sub, self.client, ext_hash)) } /// Creates and signs an extrinsic and submits to the chain for block inclusion. diff --git a/src/error.rs b/src/error.rs index f050b465a0..1ff6fc49df 100644 --- a/src/error.rs +++ b/src/error.rs @@ -63,6 +63,9 @@ pub enum Error { /// Events decoding error. #[error("Events decoding error: {0}")] EventsDecoding(#[from] EventsDecodingError), + /// Transaction progress error. + #[error("Transaction error: {0}")] + Transaction(#[from] TransactionError), /// Other error. #[error("Other error: {0}")] Other(String), @@ -158,3 +161,16 @@ pub struct PalletError { /// The error description. pub description: Vec, } + +/// Transaction error. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +pub enum TransactionError { + /// The finality subscription expired (after ~512 blocks we give up if the + /// block hasn't yet been finalized). + #[error("The finality subscription expired")] + FinalitySubscriptionTimeout, + /// The block hash that the tranaction was added to could not be found. + /// This is probably because the block was retracted before being finalized. + #[error("The block containing the transaction can no longer be found (perhaps it was on a non-finalized fork?)")] + BlockHashNotFound, +} diff --git a/src/events.rs b/src/events.rs index 112d33e776..5dbdd6bfd1 100644 --- a/src/events.rs +++ b/src/events.rs @@ -19,6 +19,7 @@ use codec::{ Compact, Decode, Encode, + Error as CodecError, Input, }; use std::marker::PhantomData; @@ -30,9 +31,9 @@ use crate::{ }, Config, Error, + Event, Metadata, Phase, - RuntimeError, }; use scale_info::{ TypeDef, @@ -56,6 +57,17 @@ pub struct RawEvent { pub data: Bytes, } +impl RawEvent { + /// Attempt to decode this [`RawEvent`] into a specific event. + pub fn as_event(&self) -> Result, CodecError> { + if self.pallet == E::PALLET && self.variant == E::EVENT { + Ok(Some(E::decode(&mut &self.data[..])?)) + } else { + Ok(None) + } + } +} + /// Events decoder. #[derive(Debug, Clone)] pub struct EventsDecoder { @@ -76,7 +88,10 @@ where } /// Decode events. - pub fn decode_events(&self, input: &mut &[u8]) -> Result, Error> { + pub fn decode_events( + &self, + input: &mut &[u8], + ) -> Result, Error> { let compact_len = >::decode(input)?; let len = compact_len.0 as usize; log::debug!("decoding {} events", len); @@ -98,13 +113,7 @@ where let event_metadata = self.metadata.event(pallet_index, variant_index)?; let mut event_data = Vec::::new(); - let mut event_errors = Vec::::new(); - let result = self.decode_raw_event( - event_metadata, - input, - &mut event_data, - &mut event_errors, - ); + let result = self.decode_raw_event(event_metadata, input, &mut event_data); let raw = match result { Ok(()) => { log::debug!("raw bytes: {}", hex::encode(&event_data),); @@ -121,18 +130,11 @@ where let topics = Vec::::decode(input)?; log::debug!("topics: {:?}", topics); - Raw::Event(event) + event } Err(err) => return Err(err), }; - - if event_errors.is_empty() { - r.push((phase.clone(), raw)); - } - - for err in event_errors { - r.push((phase.clone(), Raw::Error(err))); - } + r.push((phase.clone(), raw)); } Ok(r) } @@ -142,7 +144,6 @@ where event_metadata: &EventMetadata, input: &mut &[u8], output: &mut Vec, - errors: &mut Vec, ) -> Result<(), Error> { log::debug!( "Decoding Event '{}::{}'", @@ -151,24 +152,6 @@ where ); for arg in event_metadata.variant().fields() { let type_id = arg.ty().id(); - if event_metadata.pallet() == "System" - && event_metadata.event() == "ExtrinsicFailed" - { - let ty = self - .metadata - .resolve_type(type_id) - .ok_or(MetadataError::TypeNotFound(type_id))?; - - if ty.path().ident() == Some("DispatchError".to_string()) { - let dispatch_error = sp_runtime::DispatchError::decode(input)?; - log::info!("Dispatch Error {:?}", dispatch_error); - dispatch_error.encode_to(output); - let runtime_error = - RuntimeError::from_dispatch(&self.metadata, dispatch_error)?; - errors.push(runtime_error); - continue - } - } self.decode_type(type_id, input, output)? } Ok(()) @@ -344,15 +327,6 @@ where } } -/// Raw event or error event -#[derive(Debug)] -pub enum Raw { - /// Event - Event(RawEvent), - /// Error - Error(RuntimeError), -} - #[derive(Debug, thiserror::Error)] pub enum EventsDecodingError { /// Unsupported primitive type diff --git a/src/lib.rs b/src/lib.rs index dbd4dbeb2d..b43631128d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,7 @@ mod metadata; pub mod rpc; pub mod storage; mod subscription; +mod transaction; pub use crate::{ client::{ @@ -83,6 +84,7 @@ pub use crate::{ Error, PalletError, RuntimeError, + TransactionError, }, events::{ EventsDecoder, @@ -102,7 +104,6 @@ pub use crate::{ }, rpc::{ BlockNumber, - ExtrinsicSuccess, ReadProof, RpcClient, SystemProperties, @@ -118,6 +119,11 @@ pub use crate::{ EventSubscription, FinalizedEventStorageSubscription, }, + transaction::{ + TransactionInBlock, + TransactionProgress, + TransactionStatus, + }, }; /// Call trait. diff --git a/src/rpc.rs b/src/rpc.rs index 1e43c14cf6..39e7d8bbc5 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -26,7 +26,6 @@ use std::sync::Arc; use codec::{ Decode, Encode, - Error as CodecError, }; use core::{ convert::TryInto, @@ -67,30 +66,21 @@ use sp_core::{ Bytes, U256, }; -use sp_runtime::{ - generic::{ - Block, - SignedBlock, - }, - traits::Hash, +use sp_runtime::generic::{ + Block, + SignedBlock, }; use sp_version::RuntimeVersion; use crate::{ error::Error, - events::{ - EventsDecoder, - RawEvent, - }, storage::StorageKeyPrefix, subscription::{ EventStorageSubscription, - EventSubscription, FinalizedEventStorageSubscription, SystemEvents, }, Config, - Event, Metadata, }; @@ -152,7 +142,7 @@ pub type SystemProperties = serde_json::Map; /// must be kept compatible with that type from the target substrate version. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub enum TransactionStatus { +pub enum SubstrateTransactionStatus { /// Transaction is part of the future queue. Future, /// Transaction is part of the ready queue. @@ -293,7 +283,6 @@ pub struct Rpc { /// Rpc client for sending requests. pub client: RpcClient, marker: PhantomData, - accept_weak_inclusion: bool, } impl Clone for Rpc { @@ -301,7 +290,6 @@ impl Clone for Rpc { Self { client: self.client.clone(), marker: PhantomData, - accept_weak_inclusion: self.accept_weak_inclusion, } } } @@ -312,16 +300,9 @@ impl Rpc { Self { client, marker: PhantomData, - accept_weak_inclusion: false, } } - /// Configure the Rpc to accept non-finalized blocks - /// in `submit_and_watch_extrinsic` - pub fn accept_weak_inclusion(&mut self) { - self.accept_weak_inclusion = true; - } - /// Fetch a storage key pub async fn storage( &self, @@ -546,7 +527,7 @@ impl Rpc { pub async fn watch_extrinsic( &self, extrinsic: E, - ) -> Result>, Error> { + ) -> Result>, Error> { let bytes: Bytes = extrinsic.encode().into(); let params = &[to_json_value(bytes)?]; let subscription = self @@ -560,99 +541,6 @@ impl Rpc { Ok(subscription) } - /// Create and submit an extrinsic and return corresponding Event if successful - pub async fn submit_and_watch_extrinsic<'a, E: Encode + 'static>( - &self, - extrinsic: E, - decoder: &'a EventsDecoder, - ) -> Result, Error> { - let ext_hash = T::Hashing::hash_of(&extrinsic); - log::info!("Submitting Extrinsic `{:?}`", ext_hash); - - let events_sub = if self.accept_weak_inclusion { - self.subscribe_events().await - } else { - self.subscribe_finalized_events().await - }?; - let mut xt_sub = self.watch_extrinsic(extrinsic).await?; - - while let Ok(Some(status)) = xt_sub.next().await { - log::info!("Received status {:?}", status); - match status { - // ignore in progress extrinsic for now - TransactionStatus::Future - | TransactionStatus::Ready - | TransactionStatus::Broadcast(_) - | TransactionStatus::Retracted(_) => continue, - TransactionStatus::InBlock(block_hash) => { - if self.accept_weak_inclusion { - return self - .process_block(events_sub, decoder, block_hash, ext_hash) - .await - } - continue - } - TransactionStatus::Invalid => return Err("Extrinsic Invalid".into()), - TransactionStatus::Usurped(_) => return Err("Extrinsic Usurped".into()), - TransactionStatus::Dropped => return Err("Extrinsic Dropped".into()), - TransactionStatus::Finalized(block_hash) => { - // read finalized blocks by default - return self - .process_block(events_sub, decoder, block_hash, ext_hash) - .await - } - TransactionStatus::FinalityTimeout(_) => { - return Err("Extrinsic FinalityTimeout".into()) - } - } - } - Err(RpcError::Custom("RPC subscription dropped".into()).into()) - } - - async fn process_block( - &self, - events_sub: EventStorageSubscription, - decoder: &EventsDecoder, - block_hash: T::Hash, - ext_hash: T::Hash, - ) -> Result, Error> { - log::info!("Fetching block {:?}", block_hash); - if let Some(signed_block) = self.block(Some(block_hash)).await? { - log::info!( - "Found block {:?}, with {} extrinsics", - block_hash, - signed_block.block.extrinsics.len() - ); - let ext_index = signed_block - .block - .extrinsics - .iter() - .position(|ext| { - let hash = T::Hashing::hash_of(ext); - hash == ext_hash - }) - .ok_or_else(|| { - Error::Other(format!( - "Failed to find Extrinsic with hash {:?}", - ext_hash, - )) - })?; - let mut sub = EventSubscription::new(events_sub, decoder); - sub.filter_extrinsic(block_hash, ext_index); - let mut events = vec![]; - while let Some(event) = sub.next().await { - events.push(event?); - } - Ok(ExtrinsicSuccess { - block: block_hash, - extrinsic: ext_hash, - events, - }) - } else { - Err(format!("Failed to find block {:?}", block_hash).into()) - } - } - /// Insert a key into the keystore. pub async fn insert_key( &self, @@ -696,35 +584,3 @@ impl Rpc { Ok(self.client.request("author_hasKey", params).await?) } } - -/// Captures data for when an extrinsic is successfully included in a block -#[derive(Debug)] -pub struct ExtrinsicSuccess { - /// Block hash. - pub block: T::Hash, - /// Extrinsic hash. - pub extrinsic: T::Hash, - /// Raw runtime events, can be decoded by the caller. - pub events: Vec, -} - -impl ExtrinsicSuccess { - /// Find the Event for the given module/variant, with raw encoded event data. - /// Returns `None` if the Event is not found. - pub fn find_event_raw(&self, module: &str, variant: &str) -> Option<&RawEvent> { - self.events - .iter() - .find(|raw| raw.pallet == module && raw.variant == variant) - } - - /// Find the Event for the given module/variant, attempting to decode the event data. - /// Returns `None` if the Event is not found. - /// Returns `Err` if the data fails to decode into the supplied type. - pub fn find_event(&self) -> Result, CodecError> { - if let Some(event) = self.find_event_raw(E::PALLET, E::EVENT) { - Ok(Some(E::decode(&mut &event.data[..])?)) - } else { - Ok(None) - } - } -} diff --git a/src/subscription.rs b/src/subscription.rs index c35771c27b..f561e97093 100644 --- a/src/subscription.rs +++ b/src/subscription.rs @@ -32,7 +32,6 @@ use crate::{ error::Error, events::{ EventsDecoder, - Raw, RawEvent, }, rpc::Rpc, @@ -48,7 +47,7 @@ pub struct EventSubscription<'a, T: Config> { block: Option, extrinsic: Option, event: Option<(&'static str, &'static str)>, - events: VecDeque, + events: VecDeque, finished: bool, } @@ -59,11 +58,11 @@ enum BlockReader<'a, T: Config> { }, /// Mock event listener for unit tests #[cfg(test)] - Mock(Box, Error>)>>), + Mock(Box, Error>)>>), } impl<'a, T: Config> BlockReader<'a, T> { - async fn next(&mut self) -> Option<(T::Hash, Result, Error>)> { + async fn next(&mut self) -> Option<(T::Hash, Result, Error>)> { match self { BlockReader::Decoder { subscription, @@ -126,10 +125,7 @@ impl<'a, T: Config> EventSubscription<'a, T> { pub async fn next(&mut self) -> Option> { loop { if let Some(raw_event) = self.events.pop_front() { - match raw_event { - Raw::Event(event) => return Some(Ok(event)), - Raw::Error(err) => return Some(Err(err.into())), - }; + return Some(Ok(raw_event)) } if self.finished { return None @@ -155,10 +151,8 @@ impl<'a, T: Config> EventSubscription<'a, T> { } } if let Some((module, variant)) = self.event { - if let Raw::Event(ref event) = raw { - if event.pallet != module || event.variant != variant { - continue - } + if raw.pallet != module || raw.variant != variant { + continue } } self.events.push_back(raw); @@ -264,8 +258,6 @@ where #[cfg(test)] mod tests { - use crate::RuntimeError; - use super::*; use sp_core::H256; #[derive(Clone)] @@ -296,51 +288,6 @@ mod tests { } } - fn raw_event(id: u8) -> RawEvent { - RawEvent { - data: sp_core::Bytes::from(Vec::new()), - pallet: "SomePallet".to_string(), - variant: "SomeVariant".to_string(), - pallet_index: id, - variant_index: id, - } - } - - fn event(id: u8) -> Raw { - Raw::Event(raw_event(id)) - } - - #[async_std::test] - async fn test_error_does_not_stop_subscription() { - let mut subscription: EventSubscription = EventSubscription { - block_reader: BlockReader::Mock(Box::new( - vec![( - H256::from([0; 32]), - Ok(vec![ - ( - Phase::ApplyExtrinsic(0), - Raw::Error(RuntimeError::BadOrigin), - ), - (Phase::ApplyExtrinsic(0), event(1)), - ]), - )] - .into_iter(), - )), - block: None, - extrinsic: None, - event: None, - events: Default::default(), - finished: false, - }; - - assert!(matches!( - subscription.next().await.unwrap().unwrap_err(), - Error::Runtime(RuntimeError::BadOrigin) - )); - assert_eq!(subscription.next().await.unwrap().unwrap(), raw_event(1)); - assert!(subscription.next().await.is_none()); - } - #[async_std::test] /// test that filters work correctly, and are independent of each other async fn test_filters() { @@ -378,7 +325,7 @@ mod tests { .iter() .take(half_len) .map(|(_, phase, event)| { - (phase.clone(), Raw::Event(event.clone())) + (phase.clone(), event.clone()) }) .collect()), ), @@ -388,7 +335,7 @@ mod tests { .iter() .skip(half_len) .map(|(_, phase, event)| { - (phase.clone(), Raw::Event(event.clone())) + (phase.clone(), event.clone()) }) .collect()), ), diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000000..5b5ef340de --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,449 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// This file is part of subxt. +// +// subxt is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// subxt is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with subxt. If not, see . + +use sp_core::storage::StorageKey; +use sp_runtime::traits::Hash; +pub use sp_runtime::traits::SignedExtension; +pub use sp_version::RuntimeVersion; + +use crate::{ + client::Client, + error::{ + Error, + TransactionError, + }, + rpc::SubstrateTransactionStatus, + subscription::SystemEvents, + Config, + Phase, +}; +use jsonrpsee::types::{ + Error as RpcError, + Subscription as RpcSubscription, +}; + +/// This struct represents a subscription to the progress of some transaction, and is +/// returned from [`crate::SubmittableExtrinsic::sign_and_submit_then_watch()`]. +#[derive(Debug)] +pub struct TransactionProgress<'client, T: Config> { + sub: Option>>, + ext_hash: T::Hash, + client: &'client Client, +} + +impl<'client, T: Config> TransactionProgress<'client, T> { + pub(crate) fn new( + sub: RpcSubscription>, + client: &'client Client, + ext_hash: T::Hash, + ) -> Self { + Self { + sub: Some(sub), + client, + ext_hash, + } + } + + /// Return the next transaction status when it's emitted. + pub async fn next(&mut self) -> Result>, Error> { + // Return `None` if the subscription has been dropped: + let sub = match &mut self.sub { + Some(sub) => sub, + None => return Ok(None), + }; + + // Return the next item otherwise: + let res = sub.next().await?; + Ok(res.map(|status| { + match status { + SubstrateTransactionStatus::Future => TransactionStatus::Future, + SubstrateTransactionStatus::Ready => TransactionStatus::Ready, + SubstrateTransactionStatus::Broadcast(peers) => { + TransactionStatus::Broadcast(peers) + } + SubstrateTransactionStatus::InBlock(hash) => { + TransactionStatus::InBlock(TransactionInBlock { + block_hash: hash, + ext_hash: self.ext_hash, + client: self.client, + }) + } + SubstrateTransactionStatus::Retracted(hash) => { + TransactionStatus::Retracted(hash) + } + SubstrateTransactionStatus::Usurped(hash) => { + TransactionStatus::Usurped(hash) + } + SubstrateTransactionStatus::Dropped => TransactionStatus::Dropped, + SubstrateTransactionStatus::Invalid => TransactionStatus::Invalid, + // Only the following statuses are actually considered "final" (see the substrate + // docs on `TransactionStatus`). Basically, either the transaction makes it into a + // block, or we eventually give up on waiting for it to make it into a block. + // Even `Dropped`/`Invalid`/`Usurped` transactions might make it into a block eventually. + // + // As an example, a transaction that is `Invalid` on one node due to having the wrong + // nonce might still be valid on some fork on another node which ends up being finalized. + // Equally, a transaction `Dropped` from one node may still be in the transaction pool, + // and make it into a block, on another node. Likewise with `Usurped`. + SubstrateTransactionStatus::FinalityTimeout(hash) => { + self.sub = None; + TransactionStatus::FinalityTimeout(hash) + } + SubstrateTransactionStatus::Finalized(hash) => { + self.sub = None; + TransactionStatus::Finalized(TransactionInBlock { + block_hash: hash, + ext_hash: self.ext_hash, + client: self.client, + }) + } + } + })) + } + + /// Wait for the transaction to be in a block (but not necessarily finalized), and return + /// an [`TransactionInBlock`] instance when this happens, or an error if there was a problem + /// waiting for this to happen. + /// + /// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the + /// transaction progresses, use [`TransactionProgress::next()`] instead. + /// + /// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they + /// may well indicate with some probability that the transaction will not make it into a block, + /// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower + /// level [`TransactionProgress::next()`] API if you'd like to handle these statuses yourself. + pub async fn wait_for_in_block( + mut self, + ) -> Result, Error> { + while let Some(status) = self.next().await? { + match status { + // Finalized or otherwise in a block! Return. + TransactionStatus::InBlock(s) | TransactionStatus::Finalized(s) => { + return Ok(s) + } + // Error scenarios; return the error. + TransactionStatus::FinalityTimeout(_) => { + return Err(TransactionError::FinalitySubscriptionTimeout.into()) + } + // Ignore anything else and wait for next status event: + _ => continue, + } + } + Err(RpcError::Custom("RPC subscription dropped".into()).into()) + } + + /// Wait for the transaction to be finalized, and return a [`TransactionInBlock`] + /// instance when it is, or an error if there was a problem waiting for finalization. + /// + /// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the + /// transaction progresses, use [`TransactionProgress::next()`] instead. + /// + /// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they + /// may well indicate with some probability that the transaction will not make it into a block, + /// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower + /// level [`TransactionProgress::next()`] API if you'd like to handle these statuses yourself. + pub async fn wait_for_finalized( + mut self, + ) -> Result, Error> { + while let Some(status) = self.next().await? { + match status { + // Finalized! Return. + TransactionStatus::Finalized(s) => return Ok(s), + // Error scenarios; return the error. + TransactionStatus::FinalityTimeout(_) => { + return Err(TransactionError::FinalitySubscriptionTimeout.into()) + } + // Ignore and wait for next status event: + _ => continue, + } + } + Err(RpcError::Custom("RPC subscription dropped".into()).into()) + } + + /// Wait for the transaction to be finalized, and for the transaction events to indicate + /// that the transaction was successful. Returns the events associated with the transaction, + /// as well as a couple of other details (block hash and extrinsic hash). + /// + /// **Note:** consumes self. If you'd like to perform multiple actions as progress is made, + /// use [`TransactionProgress::next()`] instead. + /// + /// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they + /// may well indicate with some probability that the transaction will not make it into a block, + /// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower + /// level [`TransactionProgress::next()`] API if you'd like to handle these statuses yourself. + pub async fn wait_for_finalized_success(self) -> Result, Error> { + let evs = self.wait_for_finalized().await?.wait_for_success().await?; + Ok(evs) + } +} + +//* Dev note: The below is adapted from the substrate docs on `TransactionStatus`, which this +//* enum was adapted from (and which is an exact copy of `SubstrateTransactionStatus` in this crate). +//* Note that the number of finality watchers is, at the time of writing, found in the constant +//* `MAX_FINALITY_WATCHERS` in the `sc_transaction_pool` crate. +//* +/// Possible transaction statuses returned from our [`TransactionProgress::next()`] call. +/// +/// These status events can be grouped based on their kinds as: +/// +/// 1. Entering/Moving within the pool: +/// - `Future` +/// - `Ready` +/// 2. Inside `Ready` queue: +/// - `Broadcast` +/// 3. Leaving the pool: +/// - `InBlock` +/// - `Invalid` +/// - `Usurped` +/// - `Dropped` +/// 4. Re-entering the pool: +/// - `Retracted` +/// 5. Block finalized: +/// - `Finalized` +/// - `FinalityTimeout` +/// +/// The events will always be received in the order described above, however +/// there might be cases where transactions alternate between `Future` and `Ready` +/// pool, and are `Broadcast` in the meantime. +/// +/// Note that there are conditions that may cause transactions to reappear in the pool: +/// +/// 1. Due to possible forks, the transaction that ends up being included +/// in one block may later re-enter the pool or be marked as invalid. +/// 2. A transaction that is `Dropped` at one point may later re-enter the pool if +/// some other transactions are removed. +/// 3. `Invalid` transactions may become valid at some point in the future. +/// (Note that runtimes are encouraged to use `UnknownValidity` to inform the +/// pool about such cases). +/// 4. `Retracted` transactions might be included in a future block. +/// +/// The stream is considered finished only when either the `Finalized` or `FinalityTimeout` +/// event is triggered. You are however free to unsubscribe from notifications at any point. +/// The first one will be emitted when the block in which the transaction was included gets +/// finalized. The `FinalityTimeout` event will be emitted when the block did not reach finality +/// within 512 blocks. This either indicates that finality is not available for your chain, +/// or that finality gadget is lagging behind. +#[derive(Debug)] +pub enum TransactionStatus<'client, T: Config> { + /// The transaction is part of the "future" queue. + Future, + /// The transaction is part of the "ready" queue. + Ready, + /// The transaction has been broadcast to the given peers. + Broadcast(Vec), + /// The transaction has been included in a block with given hash. + InBlock(TransactionInBlock<'client, T>), + /// The block this transaction was included in has been retracted, + /// probably because it did not make it onto the blocks which were + /// finalized. + Retracted(T::Hash), + /// A block containing the transaction did not reach finality within 512 + /// blocks, and so the subscription has ended. + FinalityTimeout(T::Hash), + /// The transaction has been finalized by a finality-gadget, e.g GRANDPA. + Finalized(TransactionInBlock<'client, T>), + /// The transaction has been replaced in the pool by another transaction + /// that provides the same tags. (e.g. same (sender, nonce)). + Usurped(T::Hash), + /// The transaction has been dropped from the pool because of the limit. + Dropped, + /// The transaction is no longer valid in the current state. + Invalid, +} + +impl<'client, T: Config> TransactionStatus<'client, T> { + /// A convenience method to return the `Finalized` details. Returns + /// [`None`] if the enum variant is not [`TransactionStatus::Finalized`]. + pub fn as_finalized(&self) -> Option<&TransactionInBlock<'client, T>> { + match self { + Self::Finalized(val) => Some(val), + _ => None, + } + } + + /// A convenience method to return the `InBlock` details. Returns + /// [`None`] if the enum variant is not [`TransactionStatus::InBlock`]. + pub fn as_in_block(&self) -> Option<&TransactionInBlock<'client, T>> { + match self { + Self::InBlock(val) => Some(val), + _ => None, + } + } +} + +/// This struct represents a transaction that has made it into a block. +#[derive(Debug)] +pub struct TransactionInBlock<'client, T: Config> { + block_hash: T::Hash, + ext_hash: T::Hash, + client: &'client Client, +} + +impl<'client, T: Config> TransactionInBlock<'client, T> { + /// Return the hash of the block that the transaction has made it into. + pub fn block_hash(&self) -> T::Hash { + self.block_hash + } + + /// Return the hash of the extrinsic that was submitted. + pub fn extrinsic_hash(&self) -> T::Hash { + self.ext_hash + } + + /// Fetch the events associated with this transaction. If the transaction + /// was successful (ie no `ExtrinsicFailed`) events were found, then we return + /// the events associated with it. If the transaction was not successful, or + /// something else went wrong, we return an error. + /// + /// **Note:** If multiple `ExtrinsicFailed` errors are returned (for instance + /// because a pallet chooses to emit one as an event, which is considered + /// abnormal behaviour), it is not specified which of the errors is returned here. + /// You can use [`TransactionInBlock::fetch_events`] instead if you'd like to + /// work with multiple "error" events. + /// + /// **Note:** This has to download block details from the node and decode events + /// from them. + pub async fn wait_for_success(&self) -> Result, Error> { + let events = self.fetch_events().await?; + + // Try to find any errors; return the first one we encounter. + for ev in events.as_slice() { + if &ev.pallet == "System" && &ev.variant == "ExtrinsicFailed" { + use codec::Decode; + let dispatch_error = sp_runtime::DispatchError::decode(&mut &*ev.data)?; + let runtime_error = crate::RuntimeError::from_dispatch( + self.client.metadata(), + dispatch_error, + )?; + return Err(runtime_error.into()) + } + } + + Ok(events) + } + + /// Fetch all of the events associated with this transaction. This succeeds whether + /// the transaction was a success or not; it's up to you to handle the error and + /// success events however you prefer. + /// + /// **Note:** This has to download block details from the node and decode events + /// from them. + pub async fn fetch_events(&self) -> Result, Error> { + let block = self + .client + .rpc() + .block(Some(self.block_hash)) + .await? + .ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?; + + let extrinsic_idx = block.block.extrinsics + .iter() + .position(|ext| { + let hash = T::Hashing::hash_of(ext); + hash == self.ext_hash + }) + // If we successfully obtain the block hash we think contains our + // extrinsic, the extrinsic should be in there somewhere.. + .ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?; + + let raw_events = self + .client + .rpc() + .storage( + &StorageKey::from(SystemEvents::new()), + Some(self.block_hash), + ) + .await? + .map(|s| s.0) + .unwrap_or_else(Vec::new); + + let events = self + .client + .events_decoder() + .decode_events(&mut &*raw_events)? + .into_iter() + .filter(move |(phase, _raw)| { + phase == &Phase::ApplyExtrinsic(extrinsic_idx as u32) + }) + .map(|(_phase, event)| event) + .collect(); + + Ok(TransactionEvents { + block_hash: self.block_hash, + ext_hash: self.ext_hash, + events, + }) + } +} + +/// This represents the events related to our transaction. +/// We can iterate over the events, or look for a specific one. +#[derive(Debug)] +pub struct TransactionEvents { + block_hash: T::Hash, + ext_hash: T::Hash, + events: Vec, +} + +impl TransactionEvents { + /// Return the hash of the block that the transaction has made it into. + pub fn block_hash(&self) -> T::Hash { + self.block_hash + } + + /// Return the hash of the extrinsic. + pub fn extrinsic_hash(&self) -> T::Hash { + self.ext_hash + } + + /// Return a slice of the returned events. + pub fn as_slice(&self) -> &[crate::RawEvent] { + &self.events + } + + /// Find all of the events matching the event type provided as a generic parameter. + pub fn find_events(&self) -> Result, Error> { + self.events + .iter() + .filter_map(|e| e.as_event::().map_err(Into::into).transpose()) + .collect() + } + + /// Find the first event that matches the event type provided as a generic parameter. + /// + /// Use [`TransactionEvents::find_events`], or iterate over [`TransactionEvents`] yourself + /// if you'd like to handle multiple events of the same type. + pub fn find_first_event(&self) -> Result, Error> { + self.events + .iter() + .filter_map(|e| e.as_event::().transpose()) + .next() + .transpose() + .map_err(Into::into) + } + + /// Find an event. Returns true if it was found. + pub fn has_event(self) -> Result { + Ok(self.find_first_event::()?.is_some()) + } +} + +impl std::ops::Deref for TransactionEvents { + type Target = [crate::RawEvent]; + fn deref(&self) -> &Self::Target { + &self.events + } +} diff --git a/test-runtime/build.rs b/test-runtime/build.rs index bc35b7cc76..e0b2c33ba4 100644 --- a/test-runtime/build.rs +++ b/test-runtime/build.rs @@ -92,7 +92,7 @@ async fn run() { fs::write(&metadata_path, &metadata_bytes.0).expect("Couldn't write metadata output"); // Write out our expression to generate the runtime API to a file. Ideally, we'd just write this code - // in lib.rs, but we must pass a string literal (and not `concat!(..)`) as an arg to runtime_metadata_path, + // in lib.rs, but we must pass a string literal (and not `concat!(..)`) as an arg to `runtime_metadata_path`, // and so we need to spit it out here and include it verbatim instead. let runtime_api_contents = format!( r#" diff --git a/tests/integration/frame/balances.rs b/tests/integration/frame/balances.rs index 8b12790ffa..4df9a16c99 100644 --- a/tests/integration/frame/balances.rs +++ b/tests/integration/frame/balances.rs @@ -41,7 +41,7 @@ use subxt::{ }; #[async_std::test] -async fn tx_basic_transfer() { +async fn tx_basic_transfer() -> Result<(), subxt::Error> { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = PairSigner::::new(AccountKeyring::Bob.pair()); let bob_address = bob.account_id().clone().into(); @@ -52,28 +52,27 @@ async fn tx_basic_transfer() { .storage() .system() .account(alice.account_id().clone(), None) - .await - .unwrap(); + .await?; let bob_pre = api .storage() .system() .account(bob.account_id().clone(), None) - .await - .unwrap(); + .await?; - let result = api + let events = api .tx() .balances() .transfer(bob_address, 10_000) .sign_and_submit_then_watch(&alice) - .await - .unwrap(); - let event = result - .find_event::() - .unwrap() - .unwrap(); - let _extrinsic_success = result - .find_event::() + .await? + .wait_for_finalized_success() + .await?; + let event = events + .find_first_event::() + .expect("Failed to decode balances::events::Transfer") + .expect("Failed to find balances::events::Transfer"); + let _extrinsic_success = events + .find_first_event::() .expect("Failed to decode ExtrinisicSuccess") .expect("Failed to find ExtrinisicSuccess"); @@ -88,17 +87,16 @@ async fn tx_basic_transfer() { .storage() .system() .account(alice.account_id().clone(), None) - .await - .unwrap(); + .await?; let bob_post = api .storage() .system() .account(bob.account_id().clone(), None) - .await - .unwrap(); + .await?; assert!(alice_pre.data.free - 10_000 >= alice_post.data.free); assert_eq!(bob_pre.data.free + 10_000, bob_post.data.free); + Ok(()) } #[async_std::test] @@ -120,8 +118,7 @@ async fn storage_balance_lock() -> Result<(), subxt::Error> { let charlie = AccountKeyring::Charlie.to_account_id(); let cxt = test_context().await; - let result = cxt - .api + cxt.api .tx() .staking() .bond( @@ -130,10 +127,11 @@ async fn storage_balance_lock() -> Result<(), subxt::Error> { runtime_types::pallet_staking::RewardDestination::Stash, ) .sign_and_submit_then_watch(&bob) - .await?; - - let success = result.find_event::()?; - assert!(success.is_some(), "No ExtrinsicSuccess Event found"); + .await? + .wait_for_finalized_success() + .await? + .find_first_event::()? + .expect("No ExtrinsicSuccess Event found"); let locks = cxt .api @@ -169,6 +167,9 @@ async fn transfer_error() { .transfer(hans_address, 100_000_000_000_000_000) .sign_and_submit_then_watch(&alice) .await + .unwrap() + .wait_for_finalized_success() + .await .unwrap(); let res = cxt @@ -177,6 +178,9 @@ async fn transfer_error() { .balances() .transfer(alice_addr, 100_000_000_000_000_000) .sign_and_submit_then_watch(&hans) + .await + .unwrap() + .wait_for_finalized_success() .await; if let Err(Error::Runtime(RuntimeError::Module(error))) = res { @@ -187,7 +191,7 @@ async fn transfer_error() { }; assert_eq!(error, error2); } else { - panic!("expected an error"); + panic!("expected a runtime module error"); } } @@ -223,6 +227,39 @@ async fn transfer_subscription() { ); } +#[async_std::test] +async fn transfer_implicit_subscription() { + env_logger::try_init().ok(); + let alice = PairSigner::::new(AccountKeyring::Alice.pair()); + let bob = AccountKeyring::Bob.to_account_id(); + let bob_addr = bob.clone().into(); + let cxt = test_context().await; + + let event = cxt + .api + .tx() + .balances() + .transfer(bob_addr, 10_000) + .sign_and_submit_then_watch(&alice) + .await + .unwrap() + .wait_for_finalized_success() + .await + .unwrap() + .find_first_event::() + .expect("Can decode events") + .expect("Can find balance transfer event"); + + assert_eq!( + event, + balances::events::Transfer { + from: alice.account_id().clone(), + to: bob.clone(), + amount: 10_000 + } + ); +} + #[async_std::test] async fn constant_existential_deposit() { let cxt = test_context().await; diff --git a/tests/integration/frame/contracts.rs b/tests/integration/frame/contracts.rs index 3d8c424611..63fff6222d 100644 --- a/tests/integration/frame/contracts.rs +++ b/tests/integration/frame/contracts.rs @@ -35,8 +35,8 @@ use subxt::{ Client, Config, Error, - ExtrinsicSuccess, PairSigner, + TransactionProgress, }; struct ContractsTestContext { @@ -73,7 +73,7 @@ impl ContractsTestContext { "#; let code = wabt::wat2wasm(CONTRACT).expect("invalid wabt"); - let result = self + let events = self .cxt .api .tx() @@ -81,26 +81,29 @@ impl ContractsTestContext { .instantiate_with_code( 100_000_000_000_000_000, // endowment 500_000_000_000, // gas_limit + None, // storage_deposit_limit code, vec![], // data vec![], // salt ) .sign_and_submit_then_watch(&self.signer) + .await? + .wait_for_finalized_success() .await?; - let code_stored = result - .find_event::()? + let code_stored = events + .find_first_event::()? .ok_or_else(|| Error::Other("Failed to find a CodeStored event".into()))?; - let instantiated = result - .find_event::()? + let instantiated = events + .find_first_event::()? .ok_or_else(|| Error::Other("Failed to find a Instantiated event".into()))?; - let _extrinsic_success = result - .find_event::()? + let _extrinsic_success = events + .find_first_event::()? .ok_or_else(|| { Error::Other("Failed to find a ExtrinsicSuccess event".into()) })?; - log::info!(" Block hash: {:?}", result.block); + log::info!(" Block hash: {:?}", events.block_hash()); log::info!(" Code hash: {:?}", code_stored.code_hash); log::info!(" Contract address: {:?}", instantiated.contract); Ok((code_stored.code_hash, instantiated.contract)) @@ -118,16 +121,19 @@ impl ContractsTestContext { .instantiate( 100_000_000_000_000_000, // endowment 500_000_000_000, // gas_limit + None, // storage_deposit_limit code_hash, data, salt, ) .sign_and_submit_then_watch(&self.signer) + .await? + .wait_for_finalized_success() .await?; log::info!("Instantiate result: {:?}", result); let instantiated = result - .find_event::()? + .find_first_event::()? .ok_or_else(|| Error::Other("Failed to find a Instantiated event".into()))?; Ok(instantiated.contract) @@ -137,7 +143,7 @@ impl ContractsTestContext { &self, contract: AccountId, input_data: Vec, - ) -> Result, Error> { + ) -> Result, Error> { log::info!("call: {:?}", contract); let result = self .contracts_tx() @@ -145,6 +151,7 @@ impl ContractsTestContext { MultiAddress::Id(contract), 0, // value 500_000_000, // gas_limit + None, // storage_deposit_limit input_data, ) .sign_and_submit_then_watch(&self.signer) diff --git a/tests/integration/frame/staking.rs b/tests/integration/frame/staking.rs index 24071a0043..3f7b8f77a5 100644 --- a/tests/integration/frame/staking.rs +++ b/tests/integration/frame/staking.rs @@ -21,7 +21,6 @@ use crate::{ ValidatorPrefs, }, staking, - system, DefaultConfig, }, test_context, @@ -55,21 +54,19 @@ fn default_validator_prefs() -> ValidatorPrefs { } #[async_std::test] -async fn validate_with_controller_account() -> Result<(), Error> { +async fn validate_with_controller_account() { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let cxt = test_context().await; - let result = cxt - .api + cxt.api .tx() .staking() .validate(default_validator_prefs()) .sign_and_submit_then_watch(&alice) - .await?; - - let success = result.find_event::()?; - assert!(success.is_some()); - - Ok(()) + .await + .unwrap() + .wait_for_finalized_success() + .await + .expect("should be successful"); } #[async_std::test] @@ -82,6 +79,8 @@ async fn validate_not_possible_for_stash_account() -> Result<(), Error> { .staking() .validate(default_validator_prefs()) .sign_and_submit_then_watch(&alice_stash) + .await? + .wait_for_finalized_success() .await; assert_matches!(announce_validator, Err(Error::Runtime(RuntimeError::Module(module_err))) => { assert_eq!(module_err.pallet, "Staking"); @@ -91,23 +90,21 @@ async fn validate_not_possible_for_stash_account() -> Result<(), Error> { } #[async_std::test] -async fn nominate_with_controller_account() -> Result<(), Error> { +async fn nominate_with_controller_account() { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = PairSigner::::new(AccountKeyring::Bob.pair()); let cxt = test_context().await; - let result = cxt - .api + cxt.api .tx() .staking() .nominate(vec![bob.account_id().clone().into()]) .sign_and_submit_then_watch(&alice) - .await?; - - let success = result.find_event::()?; - assert!(success.is_some()); - - Ok(()) + .await + .unwrap() + .wait_for_finalized_success() + .await + .expect("should be successful"); } #[async_std::test] @@ -123,6 +120,8 @@ async fn nominate_not_possible_for_stash_account() -> Result<(), Error> { .staking() .nominate(vec![bob.account_id().clone().into()]) .sign_and_submit_then_watch(&alice_stash) + .await? + .wait_for_finalized_success() .await; assert_matches!(nomination, Err(Error::Runtime(RuntimeError::Module(module_err))) => { @@ -147,6 +146,8 @@ async fn chill_works_for_controller_only() -> Result<(), Error> { .staking() .nominate(vec![bob_stash.account_id().clone().into()]) .sign_and_submit_then_watch(&alice) + .await? + .wait_for_finalized_success() .await?; let ledger = cxt @@ -164,6 +165,8 @@ async fn chill_works_for_controller_only() -> Result<(), Error> { .staking() .chill() .sign_and_submit_then_watch(&alice_stash) + .await? + .wait_for_finalized_success() .await; assert_matches!(chill, Err(Error::Runtime(RuntimeError::Module(module_err))) => { @@ -171,15 +174,18 @@ async fn chill_works_for_controller_only() -> Result<(), Error> { assert_eq!(module_err.error, "NotController"); }); - let result = cxt + let is_chilled = cxt .api .tx() .staking() .chill() .sign_and_submit_then_watch(&alice) - .await?; - let chill = result.find_event::()?; - assert!(chill.is_some()); + .await? + .wait_for_finalized_success() + .await? + .has_event::()?; + assert!(is_chilled); + Ok(()) } @@ -198,6 +204,8 @@ async fn tx_bond() -> Result<(), Error> { RewardDestination::Stash, ) .sign_and_submit_then_watch(&alice) + .await? + .wait_for_finalized_success() .await; assert!(bond.is_ok()); @@ -212,6 +220,8 @@ async fn tx_bond() -> Result<(), Error> { RewardDestination::Stash, ) .sign_and_submit_then_watch(&alice) + .await? + .wait_for_finalized_success() .await; assert_matches!(bond_again, Err(Error::Runtime(RuntimeError::Module(module_err))) => { diff --git a/tests/integration/frame/sudo.rs b/tests/integration/frame/sudo.rs index 82c268b4e1..36e2cd843b 100644 --- a/tests/integration/frame/sudo.rs +++ b/tests/integration/frame/sudo.rs @@ -22,7 +22,6 @@ use crate::{ }, test_context, }; -use assert_matches::assert_matches; use sp_keyring::AccountKeyring; use subxt::extrinsic::PairSigner; @@ -30,7 +29,7 @@ type Call = runtime_types::node_runtime::Call; type BalancesCall = runtime_types::pallet_balances::pallet::Call; #[async_std::test] -async fn test_sudo() { +async fn test_sudo() -> Result<(), subxt::Error> { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = AccountKeyring::Bob.to_account_id().into(); let cxt = test_context().await; @@ -40,20 +39,23 @@ async fn test_sudo() { value: 10_000, }); - let res = cxt + let found_event = cxt .api .tx() .sudo() .sudo(call) .sign_and_submit_then_watch(&alice) - .await - .unwrap(); - let sudid = res.find_event::(); - assert_matches!(sudid, Ok(Some(_))) + .await? + .wait_for_finalized_success() + .await? + .has_event::()?; + + assert!(found_event); + Ok(()) } #[async_std::test] -async fn test_sudo_unchecked_weight() { +async fn test_sudo_unchecked_weight() -> Result<(), subxt::Error> { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = AccountKeyring::Bob.to_account_id().into(); let cxt = test_context().await; @@ -63,15 +65,17 @@ async fn test_sudo_unchecked_weight() { value: 10_000, }); - let res = cxt + let found_event = cxt .api .tx() .sudo() .sudo_unchecked_weight(call, 0) .sign_and_submit_then_watch(&alice) - .await - .unwrap(); + .await? + .wait_for_finalized_success() + .await? + .has_event::()?; - let sudid = res.find_event::(); - assert_matches!(sudid, Ok(Some(_))) + assert!(found_event); + Ok(()) } diff --git a/tests/integration/frame/system.rs b/tests/integration/frame/system.rs index 25f68ebd0b..e99ff878be 100644 --- a/tests/integration/frame/system.rs +++ b/tests/integration/frame/system.rs @@ -29,7 +29,7 @@ use subxt::extrinsic::{ }; #[async_std::test] -async fn storage_account() { +async fn storage_account() -> Result<(), subxt::Error> { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let cxt = test_context().await; @@ -39,23 +39,27 @@ async fn storage_account() { .system() .account(alice.account_id().clone(), None) .await; - assert_matches!(account_info, Ok(_)) + + assert_matches!(account_info, Ok(_)); + Ok(()) } #[async_std::test] -async fn tx_remark_with_event() { +async fn tx_remark_with_event() -> Result<(), subxt::Error> { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let cxt = test_context().await; - let result = cxt + let found_event = cxt .api .tx() .system() .remark_with_event(b"remarkable".to_vec()) .sign_and_submit_then_watch(&alice) - .await - .unwrap(); + .await? + .wait_for_finalized_success() + .await? + .has_event::()?; - let remarked = result.find_event::(); - assert_matches!(remarked, Ok(Some(_))); + assert!(found_event); + Ok(()) }