Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement state handling for Retracted status #807

Merged
merged 12 commits into from
Oct 28, 2024
4 changes: 4 additions & 0 deletions src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ pub enum Error {
BlockHashNotFound,
/// Could not find the expected block.
BlockNotFound,
/// Operation needs events but events are missing.
EventsMissing,
/// Operation wants to add events but they are already present.
EventsAlreadyPresent,
/// Any custom Error.
Other(Box<dyn ErrorT + Send + Sync + 'static>),
}
Expand Down
95 changes: 64 additions & 31 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

*/

use ac_node_api::events::RawEventDetails;
use crate::error::FailedExtrinsicError;
use ac_node_api::{events::RawEventDetails, EventDetails, Metadata};
use alloc::{string::String, vec::Vec};
use codec::{Decode, Encode};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -60,6 +61,29 @@ impl<Hash: Encode + Decode> ExtrinsicReport<Hash> {
) -> Self {
Self { extrinsic_hash, block_hash, status, events }
}

pub fn add_events(&mut self, events: Vec<EventDetails<Hash>>) {
self.events = Some(events.into_iter().map(|event| event.to_raw()).collect());
}

/// Checks the status of the extrinsic by evaluating the events attached to the report.
/// Returns an error if the events are missing or if one of the events indicates a problem.
pub fn check_events_for_dispatch_error(&self, metadata: &Metadata) -> Result<()> {
if self.events.is_none() {
return Err(Error::EventsMissing)
}
// Check if the extrinsic was successful or not.
let events = self.events.as_ref().unwrap();
for event in events {
if let Some(dispatch_error) = event.get_associated_dispatch_error(metadata) {
return Err(Error::FailedExtrinsic(FailedExtrinsicError::new(
dispatch_error,
self.encode(),
)))
}
}
Ok(())
}
}

/// Simplified TransactionStatus to allow the user to choose until when to watch
Expand All @@ -70,6 +94,7 @@ pub enum XtStatus {
Ready = 1,
Broadcast = 2,
InBlock = 3,
Retracted = 4,
Finalized = 6,
}

Expand All @@ -78,7 +103,6 @@ pub enum XtStatus {
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum UnexpectedTxStatus {
Future,
Retracted,
FinalityTimeout,
Usurped,
Dropped,
Expand Down Expand Up @@ -119,36 +143,32 @@ pub enum TransactionStatus<Hash: Encode + Decode, BlockHash: Encode + Decode> {
impl<Hash: Encode + Decode, BlockHash: Encode + Decode> TransactionStatus<Hash, BlockHash> {
pub fn as_u8(&self) -> u8 {
match self {
TransactionStatus::Future => 0,
TransactionStatus::Ready => 1,
TransactionStatus::Broadcast(_) => 2,
TransactionStatus::InBlock(_) => 3,
TransactionStatus::Retracted(_) => 4,
TransactionStatus::FinalityTimeout(_) => 5,
TransactionStatus::Finalized(_) => 6,
TransactionStatus::Usurped(_) => 7,
TransactionStatus::Dropped => 8,
TransactionStatus::Invalid => 9,
Self::Future => 0,
Self::Ready => 1,
Self::Broadcast(_) => 2,
Self::InBlock(_) => 3,
Self::Retracted(_) => 4,
Self::FinalityTimeout(_) => 5,
Self::Finalized(_) => 6,
Self::Usurped(_) => 7,
Self::Dropped => 8,
Self::Invalid => 9,
}
}

pub fn is_expected(&self) -> Result<()> {
match self {
TransactionStatus::Ready
| TransactionStatus::Broadcast(_)
| TransactionStatus::InBlock(_)
| TransactionStatus::Finalized(_) => Ok(()),
TransactionStatus::Future => Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Future)),
TransactionStatus::Retracted(_) =>
Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Retracted)),
TransactionStatus::FinalityTimeout(_) =>
Self::Ready
| Self::Broadcast(_)
| Self::InBlock(_)
| Self::Retracted(_)
| Self::Finalized(_) => Ok(()),
Self::Future => Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Future)),
Self::FinalityTimeout(_) =>
Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::FinalityTimeout)),
TransactionStatus::Usurped(_) =>
Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Usurped)),
TransactionStatus::Dropped =>
Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Dropped)),
TransactionStatus::Invalid =>
Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Invalid)),
Self::Usurped(_) => Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Usurped)),
Self::Dropped => Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Dropped)),
Self::Invalid => Err(Error::UnexpectedTxStatus(UnexpectedTxStatus::Invalid)),
}
}

Expand All @@ -160,13 +180,26 @@ impl<Hash: Encode + Decode, BlockHash: Encode + Decode> TransactionStatus<Hash,

pub fn get_maybe_block_hash(&self) -> Option<&BlockHash> {
match self {
TransactionStatus::InBlock(block_hash) => Some(block_hash),
TransactionStatus::Retracted(block_hash) => Some(block_hash),
TransactionStatus::FinalityTimeout(block_hash) => Some(block_hash),
TransactionStatus::Finalized(block_hash) => Some(block_hash),
Self::InBlock(block_hash) => Some(block_hash),
Self::Retracted(block_hash) => Some(block_hash),
Self::FinalityTimeout(block_hash) => Some(block_hash),
Self::Finalized(block_hash) => Some(block_hash),
_ => None,
}
}

/// Returns true if the Transaction reached its final Status
// See https://github.com/paritytech/polkadot-sdk/blob/289f5bbf7a45dc0380904a435464b15ec711ed03/substrate/client/transaction-pool/api/src/lib.rs#L161
pub fn is_final(&self) -> bool {
matches!(
self,
Self::Usurped(_)
| Self::Finalized(_)
| Self::FinalityTimeout(_)
| Self::Invalid
| Self::Dropped
)
}
}

// Exact structure from
Expand Down Expand Up @@ -216,11 +249,11 @@ mod tests {
assert!(TransactionStatus::Ready.is_expected().is_ok());
assert!(TransactionStatus::Broadcast(vec![]).is_expected().is_ok());
assert!(TransactionStatus::InBlock(H256::random()).is_expected().is_ok());
assert!(TransactionStatus::Retracted(H256::random()).is_expected().is_ok());
assert!(TransactionStatus::Finalized(H256::random()).is_expected().is_ok());

// Not supported.
assert!(TransactionStatus::Future.is_expected().is_err());
assert!(TransactionStatus::Retracted(H256::random()).is_expected().is_err());
assert!(TransactionStatus::FinalityTimeout(H256::random()).is_expected().is_err());
assert!(TransactionStatus::Usurped(H256::random()).is_expected().is_err());
assert!(TransactionStatus::Dropped.is_expected().is_err());
Expand Down
38 changes: 17 additions & 21 deletions src/api/rpc_api/author.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

use crate::{
api::{rpc_api::events::FetchEvents, Error, Result},
error::FailedExtrinsicError,
rpc::{HandleSubscription, Request, Subscribe},
Api, ExtrinsicReport, TransactionStatus, XtStatus,
};
Expand Down Expand Up @@ -211,6 +210,12 @@ pub trait SubmitAndWatch {
encoded_extrinsic: &Bytes,
watch_until: XtStatus,
) -> Result<ExtrinsicReport<Self::Hash>>;

/// Query the events for the specified `report` and attaches them to the mutable report.
/// If the function fails events might still be added to the report.
///
/// This method is blocking if the sync-api feature is activated
async fn populate_events(&self, report: &mut ExtrinsicReport<Self::Hash>) -> Result<()>;
}

#[maybe_async::maybe_async(?Send)]
Expand Down Expand Up @@ -276,29 +281,20 @@ where
if watch_until < XtStatus::InBlock {
return Ok(report)
}
self.populate_events(&mut report).await?;
report.check_events_for_dispatch_error(self.metadata())?;
return Ok(report);
}

async fn populate_events(&self, report: &mut ExtrinsicReport<Self::Hash>) -> Result<()> {
haerdib marked this conversation as resolved.
Show resolved Hide resolved
if report.events.is_some() {
return Err(Error::EventsAlreadyPresent)
}
let block_hash = report.block_hash.ok_or(Error::BlockHashNotFound)?;
let extrinsic_events =
self.fetch_events_for_extrinsic(block_hash, report.extrinsic_hash).await?;

// Check if the extrinsic was succesfull or not.
let mut maybe_dispatch_error = None;
for event in &extrinsic_events {
if let Some(dispatch_error) = event.get_associated_dispatch_error() {
maybe_dispatch_error = Some(dispatch_error);
break
}
}

report.events = Some(extrinsic_events.into_iter().map(|event| event.to_raw()).collect());

if let Some(dispatch_error) = maybe_dispatch_error {
return Err(Error::FailedExtrinsic(FailedExtrinsicError::new(
dispatch_error,
report.encode(),
)))
}

Ok(report)
report.add_events(extrinsic_events);
Ok(())
}

async fn submit_and_watch_extrinsic_until_without_events<
Expand Down
43 changes: 37 additions & 6 deletions testing/async/examples/author_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,18 @@ async fn main() {
test_submit_and_watch_until_broadcast(&api, transfer_call.clone(), signer_nonce + 3),
test_submit_and_watch_until_in_block(&api, transfer_call.clone(), signer_nonce + 4),
test_submit_and_watch_until_finalized(&api, transfer_call.clone(), signer_nonce + 5),
test_submit_and_watch_until_retracted(&api, transfer_call.clone(), signer_nonce + 6),
// Test some _watch_untils_without_events. We don't need to test all, because it is tested implicitly by `submit_and_watch_extrinsic_until`
// as internal call.
test_submit_and_watch_extrinsic_until_ready_without_events(
&api,
transfer_call.clone(),
signer_nonce + 6
signer_nonce + 7
),
test_submit_and_watch_extrinsic_until_in_block_without_events(
&api,
transfer_call.clone(),
signer_nonce + 7
signer_nonce + 8
)
);
}
Expand Down Expand Up @@ -125,7 +126,7 @@ async fn test_submit_and_watch_until_in_block(
let report = api.submit_and_watch_extrinsic_until(xt, XtStatus::InBlock).await.unwrap();
assert!(report.block_hash.is_some());
assert!(matches!(report.status, TransactionStatus::InBlock(_)));
assert_associated_events_match_expected(report.events.unwrap());
assert_associated_events_match_expected(&report.events.unwrap());
println!("Success: submit_and_watch_extrinsic_until {:?}", report.status);
}

Expand All @@ -139,7 +140,23 @@ async fn test_submit_and_watch_until_finalized(
let report = api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized).await.unwrap();
assert!(report.block_hash.is_some());
assert!(matches!(report.status, TransactionStatus::Finalized(_)));
assert_associated_events_match_expected(report.events.unwrap());
assert_associated_events_match_expected(&report.events.unwrap());
println!("Success: submit_and_watch_extrinsic_until {:?}", report.status);
}

async fn test_submit_and_watch_until_retracted(
api: &MyApi,
transfer_call: RuntimeCall,
nonce: Index,
) {
std::thread::sleep(std::time::Duration::from_secs(1));
let xt = api.compose_extrinsic_offline(transfer_call, nonce);
// We wait for `Retracted`` but we cannot simulate this in a test. Therefore we will receive the status after `Retracted`
// which is `Finalized`
let report = api.submit_and_watch_extrinsic_until(xt, XtStatus::Retracted).await.unwrap();
assert!(report.block_hash.is_some());
assert!(matches!(report.status, TransactionStatus::Finalized(_)));
assert_associated_events_match_expected(&report.events.unwrap());
println!("Success: submit_and_watch_extrinsic_until {:?}", report.status);
}

Expand Down Expand Up @@ -167,16 +184,30 @@ async fn test_submit_and_watch_extrinsic_until_in_block_without_events(
// Wait a little, otherwise we may run into future
std::thread::sleep(std::time::Duration::from_secs(1));
let xt = api.compose_extrinsic_offline(transfer_call, nonce);
let report = api
let mut report = api
.submit_and_watch_extrinsic_until_without_events(xt, XtStatus::InBlock)
.await
.unwrap();
println!("Extrinsic got successfully included in Block!");
assert!(report.block_hash.is_some());
assert!(report.events.is_none());

// Should fail without events
assert!(report.check_events_for_dispatch_error(&api.metadata()).is_err());

// Now we fetch the events separately
api.populate_events(&mut report).await.unwrap();
assert!(report.events.is_some());
assert!(report.check_events_for_dispatch_error(&api.metadata()).is_ok());
let events = report.events.as_ref().unwrap();
assert_associated_events_match_expected(&events);

// Can populate events only once
let result = api.populate_events(&mut report).await;
assert!(result.is_err());
}

fn assert_associated_events_match_expected(events: Vec<RawEventDetails<Hash>>) {
fn assert_associated_events_match_expected(events: &[RawEventDetails<Hash>]) {
// First event
assert_eq!(events[0].pallet_name(), "Balances");
assert_eq!(events[0].variant_name(), "Withdraw");
Expand Down
Loading