mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 20:31:13 +00:00
Introduce Backend trait to allow different RPC (or other) backends to be implemented (#1126)
* WIP backend trait * WIP converting higher level stuff to using Backend impl * more implementing new backend trait, mainly storage focused * Get core code compiling with new backend bits * subxt crate checks passing * fix tests * cargo fmt * clippy/fixes * merging and other fixes * fix test * fix lightclient code * Fix some broken doc links * another book link fix * fix broken test when moving default_rpc_client * fix dry_run test * fix more tests; lightclient and wasm * fix wasm tests * fix some doc examples * use next() instead of next_item() * missing next_item() -> next()s * move legacy RPc methods to LegacyRpcMethods type to host generic param instead of RpcClient * standardise on all RpcClient types prefixed with Rpc, and 'raw' trait types prefixed with RawRpc so it's less ocnfusing which is which * rename fixes * doc fixes * Add back system_dryRun RPC method and rename tx.dry_run() to tx.validate(), to signal that the calls are different * Add a test that we return the correct extrinsic hash from submit() * add TransactionValid details back, and protect against out of range bytes * add test for decoding transaction validation from empty bytes * fix clippy warning
This commit is contained in:
+4
-1
@@ -21,7 +21,10 @@ pub use self::signer::PairSigner;
|
||||
|
||||
pub use self::{
|
||||
signer::Signer,
|
||||
tx_client::{PartialExtrinsic, SubmittableExtrinsic, TxClient},
|
||||
tx_client::{
|
||||
PartialExtrinsic, SubmittableExtrinsic, TransactionInvalid, TransactionUnknown, TxClient,
|
||||
ValidationResult,
|
||||
},
|
||||
tx_payload::{dynamic, BoxedPayload, DynamicPayload, Payload, TxPayload},
|
||||
tx_progress::{TxInBlock, TxProgress, TxStatus},
|
||||
};
|
||||
|
||||
+353
-27
@@ -4,21 +4,18 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use codec::{Compact, Decode, Encode};
|
||||
use derivative::Derivative;
|
||||
use sp_core_hashing::blake2_256;
|
||||
|
||||
use crate::error::DecodeError;
|
||||
use crate::{
|
||||
backend::{BackendExt, BlockRef, TransactionStatus},
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, ExtrinsicParams, ExtrinsicParamsEncoder, Hasher},
|
||||
error::{Error, MetadataError},
|
||||
tx::{Signer as SignerT, TxPayload, TxProgress},
|
||||
utils::{Encoded, PhantomDataSendSync},
|
||||
};
|
||||
|
||||
// This is returned from an API below, so expose it here.
|
||||
pub use crate::rpc::types::DryRunResult;
|
||||
use codec::{Compact, Decode, Encode};
|
||||
use derivative::Derivative;
|
||||
use sp_core_hashing::blake2_256;
|
||||
|
||||
/// A client for working with transactions.
|
||||
#[derive(Derivative)]
|
||||
@@ -172,13 +169,14 @@ where
|
||||
{
|
||||
/// Get the account nonce for a given account ID.
|
||||
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, Error> {
|
||||
let block_ref = self.client.backend().latest_best_block_ref().await?;
|
||||
let account_nonce_bytes = self
|
||||
.client
|
||||
.rpc()
|
||||
.state_call_raw(
|
||||
.backend()
|
||||
.call(
|
||||
"AccountNonceApi_account_nonce",
|
||||
Some(&account_id.encode()),
|
||||
None,
|
||||
block_ref.hash(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -429,6 +427,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate and return the hash of the extrinsic, based on the configured hasher.
|
||||
pub fn hash(&self) -> T::Hash {
|
||||
T::Hasher::hash_of(&self.encoded)
|
||||
}
|
||||
|
||||
/// Returns the SCALE encoded extrinsic bytes.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.encoded.0
|
||||
@@ -452,32 +455,91 @@ where
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn submit_and_watch(&self) -> Result<TxProgress<T, C>, Error> {
|
||||
// Get a hash of the extrinsic (we'll need this later).
|
||||
let ext_hash = T::Hasher::hash_of(&self.encoded);
|
||||
let ext_hash = self.hash();
|
||||
|
||||
// Submit and watch for transaction progress.
|
||||
let sub = self.client.rpc().watch_extrinsic(&self.encoded).await?;
|
||||
let sub = self
|
||||
.client
|
||||
.backend()
|
||||
.submit_transaction(&self.encoded.0)
|
||||
.await?;
|
||||
|
||||
Ok(TxProgress::new(sub, self.client.clone(), ext_hash))
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the chain for block inclusion.
|
||||
///
|
||||
/// Returns `Ok` with the extrinsic hash if it is valid extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the extrinsic has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
/// It's usually better to call `submit_and_watch` to get an idea of the progress of the
|
||||
/// submission and whether it's eventually successful or not. This call does not guarantee
|
||||
/// success, and is just sending the transaction to the chain.
|
||||
pub async fn submit(&self) -> Result<T::Hash, Error> {
|
||||
self.client.rpc().submit_extrinsic(&self.encoded).await
|
||||
let ext_hash = self.hash();
|
||||
let mut sub = self
|
||||
.client
|
||||
.backend()
|
||||
.submit_transaction(&self.encoded.0)
|
||||
.await?;
|
||||
|
||||
// If we get a bad status or error back straight away then error, else return the hash.
|
||||
match sub.next().await {
|
||||
Some(Ok(status)) => match status {
|
||||
TransactionStatus::Validated
|
||||
| TransactionStatus::Broadcasted { .. }
|
||||
| TransactionStatus::InBestBlock { .. }
|
||||
| TransactionStatus::InFinalizedBlock { .. } => Ok(ext_hash),
|
||||
TransactionStatus::Error { message } => {
|
||||
Err(Error::Other(format!("Transaction error: {message}")))
|
||||
}
|
||||
TransactionStatus::Invalid { message } => {
|
||||
Err(Error::Other(format!("Transaction invalid: {message}")))
|
||||
}
|
||||
TransactionStatus::Dropped { message } => {
|
||||
Err(Error::Other(format!("Transaction dropped: {message}")))
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => Err(e),
|
||||
None => Err(Error::Other(
|
||||
"Transaction broadcast was unsuccessful; stream terminated early".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the dry_run RPC, to test if it would succeed.
|
||||
/// Validate a transaction by submitting it to the relevant Runtime API. A transaction that is
|
||||
/// valid can be added to a block, but may still end up in an error state.
|
||||
///
|
||||
/// Returns `Ok` with a [`DryRunResult`], which is the result of attempting to dry run the extrinsic.
|
||||
pub async fn dry_run(&self, at: Option<T::Hash>) -> Result<DryRunResult, Error> {
|
||||
let dry_run_bytes = self.client.rpc().dry_run(self.encoded(), at).await?;
|
||||
dry_run_bytes.into_dry_run_result(&self.client.metadata())
|
||||
/// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the extrinsic.
|
||||
pub async fn validate(&self) -> Result<ValidationResult, Error> {
|
||||
let latest_block_ref = self.client.backend().latest_best_block_ref().await?;
|
||||
self.validate_at(latest_block_ref).await
|
||||
}
|
||||
|
||||
/// Validate a transaction by submitting it to the relevant Runtime API. A transaction that is
|
||||
/// valid can be added to a block, but may still end up in an error state.
|
||||
///
|
||||
/// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the extrinsic.
|
||||
pub async fn validate_at(
|
||||
&self,
|
||||
at: impl Into<BlockRef<T::Hash>>,
|
||||
) -> Result<ValidationResult, Error> {
|
||||
let block_hash = at.into().hash();
|
||||
|
||||
// Approach taken from https://github.com/paritytech/json-rpc-interface-spec/issues/55.
|
||||
let mut params = Vec::with_capacity(8 + self.encoded.0.len() + 8);
|
||||
2u8.encode_to(&mut params);
|
||||
params.extend(self.encoded().iter());
|
||||
block_hash.encode_to(&mut params);
|
||||
|
||||
let res: Vec<u8> = self
|
||||
.client
|
||||
.backend()
|
||||
.call(
|
||||
"TaggedTransactionQueue_validate_transaction",
|
||||
Some(¶ms),
|
||||
block_hash,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ValidationResult::try_from_bytes(res)
|
||||
}
|
||||
|
||||
/// This returns an estimate for what the extrinsic is expected to cost to execute, less any tips.
|
||||
@@ -485,17 +547,281 @@ where
|
||||
pub async fn partial_fee_estimate(&self) -> Result<u128, Error> {
|
||||
let mut params = self.encoded().to_vec();
|
||||
(self.encoded().len() as u32).encode_to(&mut params);
|
||||
let latest_block_ref = self.client.backend().latest_best_block_ref().await?;
|
||||
|
||||
// destructuring RuntimeDispatchInfo, see type information <https://paritytech.github.io/substrate/master/pallet_transaction_payment_rpc_runtime_api/struct.RuntimeDispatchInfo.html>
|
||||
// data layout: {weight_ref_time: Compact<u64>, weight_proof_size: Compact<u64>, class: u8, partial_fee: u128}
|
||||
let (_, _, _, partial_fee) = self
|
||||
.client
|
||||
.rpc()
|
||||
.state_call::<(Compact<u64>, Compact<u64>, u8, u128)>(
|
||||
.backend()
|
||||
.call_decoding::<(Compact<u64>, Compact<u64>, u8, u128)>(
|
||||
"TransactionPaymentApi_query_info",
|
||||
Some(¶ms),
|
||||
None,
|
||||
latest_block_ref.hash(),
|
||||
)
|
||||
.await?;
|
||||
Ok(partial_fee)
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidationResult {
|
||||
#[allow(clippy::get_first)]
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<ValidationResult, crate::Error> {
|
||||
// TaggedTransactionQueue_validate_transaction returns this:
|
||||
// https://github.com/paritytech/substrate/blob/0cdf7029017b70b7c83c21a4dc0aa1020e7914f6/primitives/runtime/src/transaction_validity.rs#L210
|
||||
// We copy some of the inner types and put the three states (valid, invalid, unknown) into one enum,
|
||||
// because from our perspective, the call was successful regardless.
|
||||
if bytes.get(0) == Some(&0) {
|
||||
// ok: valid. Decode but, for now we discard most of the information
|
||||
let res = TransactionValid::decode(&mut &bytes[1..])?;
|
||||
Ok(ValidationResult::Valid(res))
|
||||
} else if bytes.get(0) == Some(&1) && bytes.get(1) == Some(&0) {
|
||||
// error: invalid
|
||||
let res = TransactionInvalid::decode(&mut &bytes[2..])?;
|
||||
Ok(ValidationResult::Invalid(res))
|
||||
} else if bytes.get(0) == Some(&1) && bytes.get(1) == Some(&1) {
|
||||
// error: unknown
|
||||
let res = TransactionUnknown::decode(&mut &bytes[2..])?;
|
||||
Ok(ValidationResult::Unknown(res))
|
||||
} else {
|
||||
// unable to decode the bytes; they aren't what we expect.
|
||||
Err(crate::Error::Unknown(bytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of performing [`SubmittableExtrinsic::validate()`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ValidationResult {
|
||||
/// The transaction is valid
|
||||
Valid(TransactionValid),
|
||||
/// The transaction is invalid
|
||||
Invalid(TransactionInvalid),
|
||||
/// Unable to validate the transaction
|
||||
Unknown(TransactionUnknown),
|
||||
}
|
||||
|
||||
impl ValidationResult {
|
||||
/// Is the transaction valid.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
matches!(self, ValidationResult::Valid(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction is valid; here is some more information about it.
|
||||
#[derive(Decode, Clone, Debug, PartialEq)]
|
||||
pub struct TransactionValid {
|
||||
/// Priority of the transaction.
|
||||
///
|
||||
/// Priority determines the ordering of two transactions that have all
|
||||
/// their dependencies (required tags) satisfied.
|
||||
pub priority: u64,
|
||||
/// Transaction dependencies
|
||||
///
|
||||
/// A non-empty list signifies that some other transactions which provide
|
||||
/// given tags are required to be included before that one.
|
||||
pub requires: Vec<Vec<u8>>,
|
||||
/// Provided tags
|
||||
///
|
||||
/// A list of tags this transaction provides. Successfully importing the transaction
|
||||
/// will enable other transactions that depend on (require) those tags to be included as well.
|
||||
/// Provided and required tags allow Substrate to build a dependency graph of transactions
|
||||
/// and import them in the right (linear) order.
|
||||
pub provides: Vec<Vec<u8>>,
|
||||
/// Transaction longevity
|
||||
///
|
||||
/// Longevity describes minimum number of blocks the validity is correct.
|
||||
/// After this period transaction should be removed from the pool or revalidated.
|
||||
pub longevity: u64,
|
||||
/// A flag indicating if the transaction should be propagated to other peers.
|
||||
///
|
||||
/// By setting `false` here the transaction will still be considered for
|
||||
/// including in blocks that are authored on the current node, but will
|
||||
/// never be sent to other peers.
|
||||
pub propagate: bool,
|
||||
}
|
||||
|
||||
/// The runtime was unable to validate the transaction.
|
||||
#[derive(Decode, Clone, Debug, PartialEq)]
|
||||
pub enum TransactionUnknown {
|
||||
/// Could not lookup some information that is required to validate the transaction.
|
||||
CannotLookup,
|
||||
/// No validator found for the given unsigned transaction.
|
||||
NoUnsignedValidator,
|
||||
/// Any other custom unknown validity that is not covered by this enum.
|
||||
Custom(u8),
|
||||
}
|
||||
|
||||
/// The transaction is invalid.
|
||||
#[derive(Decode, Clone, Debug, PartialEq)]
|
||||
pub enum TransactionInvalid {
|
||||
/// The call of the transaction is not expected.
|
||||
Call,
|
||||
/// General error to do with the inability to pay some fees (e.g. account balance too low).
|
||||
Payment,
|
||||
/// General error to do with the transaction not yet being valid (e.g. nonce too high).
|
||||
Future,
|
||||
/// General error to do with the transaction being outdated (e.g. nonce too low).
|
||||
Stale,
|
||||
/// General error to do with the transaction's proofs (e.g. signature).
|
||||
///
|
||||
/// # Possible causes
|
||||
///
|
||||
/// When using a signed extension that provides additional data for signing, it is required
|
||||
/// that the signing and the verifying side use the same additional data. Additional
|
||||
/// data will only be used to generate the signature, but will not be part of the transaction
|
||||
/// itself. As the verifying side does not know which additional data was used while signing
|
||||
/// it will only be able to assume a bad signature and cannot express a more meaningful error.
|
||||
BadProof,
|
||||
/// The transaction birth block is ancient.
|
||||
///
|
||||
/// # Possible causes
|
||||
///
|
||||
/// For `FRAME`-based runtimes this would be caused by `current block number
|
||||
/// - Era::birth block number > BlockHashCount`. (e.g. in Polkadot `BlockHashCount` = 2400, so
|
||||
/// a
|
||||
/// transaction with birth block number 1337 would be valid up until block number 1337 + 2400,
|
||||
/// after which point the transaction would be considered to have an ancient birth block.)
|
||||
AncientBirthBlock,
|
||||
/// The transaction would exhaust the resources of current block.
|
||||
///
|
||||
/// The transaction might be valid, but there are not enough resources
|
||||
/// left in the current block.
|
||||
ExhaustsResources,
|
||||
/// Any other custom invalid validity that is not covered by this enum.
|
||||
Custom(u8),
|
||||
/// An extrinsic with a Mandatory dispatch resulted in Error. This is indicative of either a
|
||||
/// malicious validator or a buggy `provide_inherent`. In any case, it can result in
|
||||
/// dangerously overweight blocks and therefore if found, invalidates the block.
|
||||
BadMandatory,
|
||||
/// An extrinsic with a mandatory dispatch tried to be validated.
|
||||
/// This is invalid; only inherent extrinsics are allowed to have mandatory dispatches.
|
||||
MandatoryValidation,
|
||||
/// The sending address is disabled or known to be invalid.
|
||||
BadSigner,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transaction_validity_decoding_empty_bytes() {
|
||||
// No panic should occur decoding empty bytes.
|
||||
let decoded = ValidationResult::try_from_bytes(vec![]);
|
||||
assert!(decoded.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_validity_decoding_is_ok() {
|
||||
use sp_runtime::transaction_validity as sp;
|
||||
use sp_runtime::transaction_validity::TransactionValidity as T;
|
||||
|
||||
let pairs = vec![
|
||||
(
|
||||
T::Ok(sp::ValidTransaction {
|
||||
..Default::default()
|
||||
}),
|
||||
ValidationResult::Valid(TransactionValid {
|
||||
// By default, tx is immortal
|
||||
longevity: u64::MAX,
|
||||
// Default is true
|
||||
propagate: true,
|
||||
priority: 0,
|
||||
provides: vec![],
|
||||
requires: vec![],
|
||||
}),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::BadProof,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::BadProof),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Call,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Call),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Payment,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Payment),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Future,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Future),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Stale,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Stale),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::AncientBirthBlock,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::AncientBirthBlock),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::ExhaustsResources,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::ExhaustsResources),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::BadMandatory,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::BadMandatory),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::MandatoryValidation,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::MandatoryValidation),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::BadSigner,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::BadSigner),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Custom(123),
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Custom(123)),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Unknown(
|
||||
sp::UnknownTransaction::CannotLookup,
|
||||
)),
|
||||
ValidationResult::Unknown(TransactionUnknown::CannotLookup),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Unknown(
|
||||
sp::UnknownTransaction::NoUnsignedValidator,
|
||||
)),
|
||||
ValidationResult::Unknown(TransactionUnknown::NoUnsignedValidator),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Unknown(
|
||||
sp::UnknownTransaction::Custom(123),
|
||||
)),
|
||||
ValidationResult::Unknown(TransactionUnknown::Custom(123)),
|
||||
),
|
||||
];
|
||||
|
||||
for (sp, validation_result) in pairs {
|
||||
let encoded = sp.encode();
|
||||
let decoded = ValidationResult::try_from_bytes(encoded).expect("should decode OK");
|
||||
assert_eq!(decoded, validation_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+125
-197
@@ -8,24 +8,32 @@ use std::task::Poll;
|
||||
|
||||
use crate::utils::strip_compact_prefix;
|
||||
use crate::{
|
||||
backend::{StreamOfResults, TransactionStatus as BackendTxStatus},
|
||||
client::OnlineClientT,
|
||||
error::{DispatchError, Error, RpcError, TransactionError},
|
||||
events::EventsClient,
|
||||
rpc::types::{Subscription, SubstrateTxStatus},
|
||||
Config,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use futures::{Stream, StreamExt};
|
||||
|
||||
/// This struct represents a subscription to the progress of some transaction.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
|
||||
pub struct TxProgress<T: Config, C> {
|
||||
sub: Option<Subscription<SubstrateTxStatus<T::Hash, T::Hash>>>,
|
||||
sub: Option<StreamOfResults<BackendTxStatus<T::Hash>>>,
|
||||
ext_hash: T::Hash,
|
||||
client: C,
|
||||
}
|
||||
|
||||
impl<T: Config, C> std::fmt::Debug for TxProgress<T, C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TxProgress")
|
||||
.field("sub", &"<subscription>")
|
||||
.field("ext_hash", &self.ext_hash)
|
||||
.field("client", &"<client>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// The above type is not `Unpin` by default unless the generic param `T` is,
|
||||
// so we manually make it clear that Unpin is actually fine regardless of `T`
|
||||
// (we don't care if this moves around in memory while it's "pinned").
|
||||
@@ -34,7 +42,7 @@ impl<T: Config, C> Unpin for TxProgress<T, C> {}
|
||||
impl<T: Config, C> TxProgress<T, C> {
|
||||
/// Instantiate a new [`TxProgress`] from a custom subscription.
|
||||
pub fn new(
|
||||
sub: Subscription<SubstrateTxStatus<T::Hash, T::Hash>>,
|
||||
sub: StreamOfResults<BackendTxStatus<T::Hash>>,
|
||||
client: C,
|
||||
ext_hash: T::Hash,
|
||||
) -> Self {
|
||||
@@ -59,8 +67,8 @@ where
|
||||
/// Return the next transaction status when it's emitted. This just delegates to the
|
||||
/// [`futures::Stream`] implementation for [`TxProgress`], but allows you to
|
||||
/// avoid importing that trait if you don't otherwise need it.
|
||||
pub async fn next_item(&mut self) -> Option<Result<TxStatus<T, C>, Error>> {
|
||||
self.next().await
|
||||
pub async fn next(&mut self) -> Option<Result<TxStatus<T, C>, Error>> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be in a block (but not necessarily finalized), and return
|
||||
@@ -68,24 +76,25 @@ where
|
||||
/// waiting for this to happen.
|
||||
///
|
||||
/// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the
|
||||
/// transaction progresses, use [`TxProgress::next_item()`] instead.
|
||||
/// transaction progresses, use [`TxProgress::next()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some
|
||||
/// probability that the transaction will not make it into a block but there is no guarantee
|
||||
/// that this is true. In those cases the stream is closed however, so you currently have no way to find
|
||||
/// out if they finally made it into a block or not.
|
||||
pub async fn wait_for_in_block(mut self) -> Result<TxInBlock<T, C>, Error> {
|
||||
while let Some(status) = self.next_item().await {
|
||||
while let Some(status) = self.next().await {
|
||||
match status? {
|
||||
// Finalized or otherwise in a block! Return.
|
||||
TxStatus::InBlock(s) | TxStatus::Finalized(s) => return Ok(s),
|
||||
TxStatus::InBestBlock(s) | TxStatus::InFinalizedBlock(s) => return Ok(s),
|
||||
// Error scenarios; return the error.
|
||||
TxStatus::FinalityTimeout(_) => {
|
||||
return Err(TransactionError::FinalityTimeout.into());
|
||||
TxStatus::Error { message } => return Err(TransactionError::Error(message).into()),
|
||||
TxStatus::Invalid { message } => {
|
||||
return Err(TransactionError::Invalid(message).into())
|
||||
}
|
||||
TxStatus::Dropped { message } => {
|
||||
return Err(TransactionError::Dropped(message).into())
|
||||
}
|
||||
TxStatus::Invalid => return Err(TransactionError::Invalid.into()),
|
||||
TxStatus::Usurped(_) => return Err(TransactionError::Usurped.into()),
|
||||
TxStatus::Dropped => return Err(TransactionError::Dropped.into()),
|
||||
// Ignore anything else and wait for next status event:
|
||||
_ => continue,
|
||||
}
|
||||
@@ -97,24 +106,25 @@ where
|
||||
/// 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 [`TxProgress::next_item()`] instead.
|
||||
/// transaction progresses, use [`TxProgress::next()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some
|
||||
/// probability that the transaction will not make it into a block but there is no guarantee
|
||||
/// that this is true. In those cases the stream is closed however, so you currently have no way to find
|
||||
/// out if they finally made it into a block or not.
|
||||
pub async fn wait_for_finalized(mut self) -> Result<TxInBlock<T, C>, Error> {
|
||||
while let Some(status) = self.next_item().await {
|
||||
while let Some(status) = self.next().await {
|
||||
match status? {
|
||||
// Finalized! Return.
|
||||
TxStatus::Finalized(s) => return Ok(s),
|
||||
TxStatus::InFinalizedBlock(s) => return Ok(s),
|
||||
// Error scenarios; return the error.
|
||||
TxStatus::FinalityTimeout(_) => {
|
||||
return Err(TransactionError::FinalityTimeout.into());
|
||||
TxStatus::Error { message } => return Err(TransactionError::Error(message).into()),
|
||||
TxStatus::Invalid { message } => {
|
||||
return Err(TransactionError::Invalid(message).into())
|
||||
}
|
||||
TxStatus::Dropped { message } => {
|
||||
return Err(TransactionError::Dropped(message).into())
|
||||
}
|
||||
TxStatus::Invalid => return Err(TransactionError::Invalid.into()),
|
||||
TxStatus::Usurped(_) => return Err(TransactionError::Usurped.into()),
|
||||
TxStatus::Dropped => return Err(TransactionError::Dropped.into()),
|
||||
// Ignore and wait for next status event:
|
||||
_ => continue,
|
||||
}
|
||||
@@ -127,7 +137,7 @@ where
|
||||
/// 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 [`TxProgress::next_item()`] instead.
|
||||
/// use [`TxProgress::next()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some
|
||||
/// probability that the transaction will not make it into a block but there is no guarantee
|
||||
@@ -155,156 +165,84 @@ impl<T: Config, C: Clone> Stream for TxProgress<T, C> {
|
||||
|
||||
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()))
|
||||
BackendTxStatus::Validated => TxStatus::Validated,
|
||||
BackendTxStatus::Broadcasted { num_peers } => TxStatus::Broadcasted { num_peers },
|
||||
BackendTxStatus::InBestBlock { hash } => {
|
||||
TxStatus::InBestBlock(TxInBlock::new(hash, self.ext_hash, self.client.clone()))
|
||||
}
|
||||
SubstrateTxStatus::Retracted(hash) => TxStatus::Retracted(hash),
|
||||
// Only the following statuses are considered "final", in a sense that they end the stream (see the substrate
|
||||
// docs on `TxStatus`):
|
||||
//
|
||||
// - Usurped
|
||||
// - Finalized
|
||||
// - FinalityTimeout
|
||||
// - Invalid
|
||||
// - Dropped
|
||||
//
|
||||
// Even though `Dropped`/`Invalid`/`Usurped` transactions might make it into a block eventually,
|
||||
// the server considers them final and closes the connection, when they are encountered.
|
||||
// In those cases the stream is closed however, so you currently have no way to find
|
||||
// out if they finally made it into a block or not.
|
||||
//
|
||||
// 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`.
|
||||
SubstrateTxStatus::FinalityTimeout(hash) => {
|
||||
// These stream events mean that nothing further will be sent:
|
||||
BackendTxStatus::InFinalizedBlock { hash } => {
|
||||
self.sub = None;
|
||||
TxStatus::FinalityTimeout(hash)
|
||||
TxStatus::InFinalizedBlock(TxInBlock::new(
|
||||
hash,
|
||||
self.ext_hash,
|
||||
self.client.clone(),
|
||||
))
|
||||
}
|
||||
SubstrateTxStatus::Finalized(hash) => {
|
||||
BackendTxStatus::Error { message } => {
|
||||
self.sub = None;
|
||||
TxStatus::Finalized(TxInBlock::new(hash, self.ext_hash, self.client.clone()))
|
||||
TxStatus::Error { message }
|
||||
}
|
||||
SubstrateTxStatus::Usurped(hash) => {
|
||||
BackendTxStatus::Invalid { message } => {
|
||||
self.sub = None;
|
||||
TxStatus::Usurped(hash)
|
||||
TxStatus::Invalid { message }
|
||||
}
|
||||
SubstrateTxStatus::Dropped => {
|
||||
BackendTxStatus::Dropped { message } => {
|
||||
self.sub = None;
|
||||
TxStatus::Dropped
|
||||
}
|
||||
SubstrateTxStatus::Invalid => {
|
||||
self.sub = None;
|
||||
TxStatus::Invalid
|
||||
TxStatus::Dropped { message }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//* Dev note: The below is adapted from the substrate docs on `TxStatus`, which this
|
||||
//* enum was adapted from (and which is an exact copy of `SubstrateTxStatus` 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 [`TxProgress::next_item()`] 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.
|
||||
///
|
||||
/// You are 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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Even though these cases can happen, the server-side of the stream is closed, if one of the following is encountered:
|
||||
/// - Usurped
|
||||
/// - Finalized
|
||||
/// - FinalityTimeout
|
||||
/// - Invalid
|
||||
/// - Dropped
|
||||
///
|
||||
/// In any of these cases the client side TxProgress stream is also closed.
|
||||
/// In those cases the stream is closed however, so you currently have no way to find
|
||||
/// out if they finally made it into a block or not.
|
||||
/// Possible transaction statuses returned from our [`TxProgress::next()`] call.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
|
||||
pub enum TxStatus<T: Config, C> {
|
||||
/// 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<String>),
|
||||
/// The transaction has been included in a block with given hash.
|
||||
InBlock(TxInBlock<T, C>),
|
||||
/// 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(TxInBlock<T, C>),
|
||||
/// 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,
|
||||
/// Transaction is part of the future queue.
|
||||
Validated,
|
||||
/// The transaction has been broadcast to other nodes.
|
||||
Broadcasted {
|
||||
/// Number of peers it's been broadcast to.
|
||||
num_peers: u32,
|
||||
},
|
||||
/// Transaction has been included in block with given hash.
|
||||
InBestBlock(TxInBlock<T, C>),
|
||||
/// Transaction has been finalized by a finality-gadget, e.g GRANDPA
|
||||
InFinalizedBlock(TxInBlock<T, C>),
|
||||
/// Something went wrong in the node.
|
||||
Error {
|
||||
/// Human readable message; what went wrong.
|
||||
message: String,
|
||||
},
|
||||
/// Transaction is invalid (bad nonce, signature etc).
|
||||
Invalid {
|
||||
/// Human readable message; why was it invalid.
|
||||
message: String,
|
||||
},
|
||||
/// The transaction was dropped.
|
||||
Dropped {
|
||||
/// Human readable message; why was it dropped.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Config, C> TxStatus<T, C> {
|
||||
/// A convenience method to return the `Finalized` details. Returns
|
||||
/// [`None`] if the enum variant is not [`TxStatus::Finalized`].
|
||||
/// A convenience method to return the finalized details. Returns
|
||||
/// [`None`] if the enum variant is not [`TxStatus::InFinalizedBlock`].
|
||||
pub fn as_finalized(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::Finalized(val) => Some(val),
|
||||
Self::InFinalizedBlock(val) => Some(val),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A convenience method to return the `InBlock` details. Returns
|
||||
/// [`None`] if the enum variant is not [`TxStatus::InBlock`].
|
||||
/// A convenience method to return the best block details. Returns
|
||||
/// [`None`] if the enum variant is not [`TxStatus::InBestBlock`].
|
||||
pub fn as_in_block(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::InBlock(val) => Some(val),
|
||||
Self::InBestBlock(val) => Some(val),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -376,20 +314,18 @@ impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
||||
/// **Note:** This has to download block details from the node and decode events
|
||||
/// from them.
|
||||
pub async fn fetch_events(&self) -> Result<crate::blocks::ExtrinsicEvents<T>, Error> {
|
||||
let block = self
|
||||
let block_body = self
|
||||
.client
|
||||
.rpc()
|
||||
.block(Some(self.block_hash))
|
||||
.backend()
|
||||
.block_body(self.block_hash)
|
||||
.await?
|
||||
.ok_or(Error::Transaction(TransactionError::BlockNotFound))?;
|
||||
|
||||
let extrinsic_idx = block
|
||||
.block
|
||||
.extrinsics
|
||||
let extrinsic_idx = block_body
|
||||
.iter()
|
||||
.position(|ext| {
|
||||
use crate::config::Hasher;
|
||||
let Ok((_, stripped)) = strip_compact_prefix(&ext.0) else {
|
||||
let Ok((_,stripped)) = strip_compact_prefix(ext) else {
|
||||
return false;
|
||||
};
|
||||
let hash = T::Hasher::hash_of(&stripped);
|
||||
@@ -413,23 +349,16 @@ impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::pin::Pin;
|
||||
|
||||
use futures::Stream;
|
||||
|
||||
use crate::{
|
||||
backend::{StreamOfResults, TransactionStatus},
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
error::RpcError,
|
||||
rpc::{types::SubstrateTxStatus, RpcSubscription, Subscription},
|
||||
tx::TxProgress,
|
||||
Config, Error, SubstrateConfig,
|
||||
};
|
||||
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
type MockTxProgress = TxProgress<SubstrateConfig, MockClient>;
|
||||
type MockHash = <SubstrateConfig as Config>::Hash;
|
||||
type MockSubstrateTxStatus = SubstrateTxStatus<MockHash, MockHash>;
|
||||
type MockSubstrateTxStatus = TransactionStatus<MockHash>;
|
||||
|
||||
/// a mock client to satisfy trait bounds in tests
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -444,49 +373,59 @@ mod test {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
|
||||
fn runtime_version(&self) -> crate::rpc::types::RuntimeVersion {
|
||||
fn runtime_version(&self) -> crate::backend::RuntimeVersion {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
}
|
||||
|
||||
impl OnlineClientT<SubstrateConfig> for MockClient {
|
||||
fn rpc(&self) -> &crate::rpc::Rpc<SubstrateConfig> {
|
||||
fn backend(&self) -> &dyn crate::backend::Backend<SubstrateConfig> {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_finalized_returns_err_when_usurped() {
|
||||
async fn wait_for_finalized_returns_err_when_error() {
|
||||
let tx_progress = mock_tx_progress(vec![
|
||||
SubstrateTxStatus::Ready,
|
||||
SubstrateTxStatus::Usurped(Default::default()),
|
||||
MockSubstrateTxStatus::Broadcasted { num_peers: 2 },
|
||||
MockSubstrateTxStatus::Error {
|
||||
message: "err".into(),
|
||||
},
|
||||
]);
|
||||
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))
|
||||
Err(Error::Transaction(crate::error::TransactionError::Error(e))) if e == "err"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_finalized_returns_err_when_invalid() {
|
||||
let tx_progress =
|
||||
mock_tx_progress(vec![SubstrateTxStatus::Ready, SubstrateTxStatus::Invalid]);
|
||||
let tx_progress = mock_tx_progress(vec![
|
||||
MockSubstrateTxStatus::Broadcasted { num_peers: 2 },
|
||||
MockSubstrateTxStatus::Invalid {
|
||||
message: "err".into(),
|
||||
},
|
||||
]);
|
||||
let finalized_result = tx_progress.wait_for_finalized().await;
|
||||
assert!(matches!(
|
||||
finalized_result,
|
||||
Err(Error::Transaction(crate::error::TransactionError::Invalid))
|
||||
Err(Error::Transaction(crate::error::TransactionError::Invalid(e))) if e == "err"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_finalized_returns_err_when_dropped() {
|
||||
let tx_progress = mock_tx_progress(vec![
|
||||
MockSubstrateTxStatus::Broadcasted { num_peers: 2 },
|
||||
MockSubstrateTxStatus::Dropped {
|
||||
message: "err".into(),
|
||||
},
|
||||
]);
|
||||
let finalized_result = tx_progress.wait_for_finalized().await;
|
||||
assert!(matches!(
|
||||
finalized_result,
|
||||
Err(Error::Transaction(crate::error::TransactionError::Dropped(e))) if e == "err"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -497,21 +436,10 @@ mod test {
|
||||
|
||||
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);
|
||||
) -> StreamOfResults<MockSubstrateTxStatus> {
|
||||
let results = elements.into_iter().map(Ok);
|
||||
let stream = Box::pin(futures::stream::iter(results));
|
||||
let sub: StreamOfResults<MockSubstrateTxStatus> = StreamOfResults::new(stream);
|
||||
sub
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user