feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo

- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt
- Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk
- Convert git dependencies to path dependencies
- Add vendor crates to workspace members
- Remove test/example crates from vendor (not needed for SDK)
- Fix feature propagation issues detected by zepter
- Fix workspace inheritance for internal dependencies
- All 606 crates now in workspace
- All 6919 internal dependency links verified correct
- No git dependencies remaining
This commit is contained in:
2025-12-22 23:31:24 +03:00
parent 4c8f281051
commit 70ddb6516f
386 changed files with 76759 additions and 36 deletions
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Create and submit extrinsics.
//!
//! An extrinsic is submitted with an "signed extra" and "additional" parameters, which can be
//! different for each chain. The trait [`crate::config::ExtrinsicParams`] determines exactly which
//! additional and signed extra parameters are used when constructing an extrinsic, and is a part
//! of the chain configuration (see [`crate::config::Config`]).
mod tx_client;
mod tx_progress;
pub use pezkuwi_subxt_core::tx::{
payload::{DefaultPayload, DynamicPayload, Payload, dynamic},
signer::{self, Signer},
};
pub use tx_client::{
DefaultParams, PartialTransaction, SubmittableTransaction, TransactionInvalid,
TransactionUnknown, TxClient, ValidationResult,
};
pub use tx_progress::{TxInBlock, TxProgress, TxStatus};
+950
View File
@@ -0,0 +1,950 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{
backend::{BackendExt, BlockRef, TransactionStatus},
client::{OfflineClientT, OnlineClientT},
config::{Config, ExtrinsicParams, HashFor, Header},
error::{ExtrinsicError, TransactionStatusError},
tx::{Payload, Signer as SignerT, TxProgress},
utils::PhantomDataSendSync,
};
use codec::{Compact, Decode, Encode};
use derive_where::derive_where;
use futures::future::{TryFutureExt, try_join};
use pezkuwi_subxt_core::tx::TransactionVersion;
/// A client for working with transactions.
#[derive_where(Clone; Client)]
pub struct TxClient<T: Config, Client> {
client: Client,
_marker: PhantomDataSendSync<T>,
}
impl<T: Config, Client> TxClient<T, Client> {
/// Create a new [`TxClient`]
pub fn new(client: Client) -> Self {
Self { client, _marker: PhantomDataSendSync::new() }
}
}
impl<T: Config, C: OfflineClientT<T>> TxClient<T, C> {
/// Run the validation logic against some transaction you'd like to submit. Returns `Ok(())`
/// if the call is valid (or if it's not possible to check since the call has no validation
/// hash). Return an error if the call was not valid or something went wrong trying to validate
/// it (ie the pallet or call in question do not exist at all).
pub fn validate<Call>(&self, call: &Call) -> Result<(), ExtrinsicError>
where
Call: Payload,
{
pezkuwi_subxt_core::tx::validate(call, &self.client.metadata()).map_err(Into::into)
}
/// Return the SCALE encoded bytes representing the call data of the transaction.
pub fn call_data<Call>(&self, call: &Call) -> Result<Vec<u8>, ExtrinsicError>
where
Call: Payload,
{
pezkuwi_subxt_core::tx::call_data(call, &self.client.metadata()).map_err(Into::into)
}
/// Creates an unsigned transaction without submitting it. Depending on the metadata, we might
/// end up constructing either a v4 or v5 transaction. See [`Self::create_v4_unsigned`] or
/// [`Self::create_v5_bare`] if you'd like to explicitly create an unsigned transaction of a
/// certain version.
pub fn create_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = match pezkuwi_subxt_core::tx::suggested_version(&metadata)? {
TransactionVersion::V4 => pezkuwi_subxt_core::tx::create_v4_unsigned(call, &metadata),
TransactionVersion::V5 => pezkuwi_subxt_core::tx::create_v5_bare(call, &metadata),
}?;
Ok(SubmittableTransaction { client: self.client.clone(), inner: tx })
}
/// Creates a v4 unsigned (no signature or transaction extensions) transaction without
/// submitting it.
///
/// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick
/// the most suitable one for the given chain.
pub fn create_v4_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = pezkuwi_subxt_core::tx::create_v4_unsigned(call, &metadata)?;
Ok(SubmittableTransaction { client: self.client.clone(), inner: tx })
}
/// Creates a v5 "bare" (no signature or transaction extensions) transaction without submitting
/// it.
///
/// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick
/// the most suitable one for the given chain.
pub fn create_v5_bare<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = pezkuwi_subxt_core::tx::create_v5_bare(call, &metadata)?;
Ok(SubmittableTransaction { client: self.client.clone(), inner: tx })
}
/// Create a partial transaction. Depending on the metadata, we might end up constructing either
/// a v4 or v5 transaction. See [`pezkuwi_subxt_core::tx`] if you'd like to manually pick the
/// version to construct
///
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality
/// will be _immortal_. This is because this method runs offline, and so is unable to fetch the
/// data needed for more appropriate values.
pub fn create_partial_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = match pezkuwi_subxt_core::tx::suggested_version(&metadata)? {
TransactionVersion::V4 =>
PartialTransactionInner::V4(pezkuwi_subxt_core::tx::create_v4_signed(
call,
&self.client.client_state(),
params,
)?),
TransactionVersion::V5 =>
PartialTransactionInner::V5(pezkuwi_subxt_core::tx::create_v5_general(
call,
&self.client.client_state(),
params,
)?),
};
Ok(PartialTransaction { client: self.client.clone(), inner: tx })
}
/// Create a v4 partial transaction, ready to sign.
///
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality
/// will be _immortal_. This is because this method runs offline, and so is unable to fetch the
/// data needed for more appropriate values.
///
/// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this
/// will pick the most suitable one for the given chain.
pub fn create_v4_partial_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let tx = PartialTransactionInner::V4(pezkuwi_subxt_core::tx::create_v4_signed(
call,
&self.client.client_state(),
params,
)?);
Ok(PartialTransaction { client: self.client.clone(), inner: tx })
}
/// Create a v5 partial transaction, ready to sign.
///
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality
/// will be _immortal_. This is because this method runs offline, and so is unable to fetch the
/// data needed for more appropriate values.
///
/// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this
/// will pick the most suitable one for the given chain.
pub fn create_v5_partial_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let tx = PartialTransactionInner::V5(pezkuwi_subxt_core::tx::create_v5_general(
call,
&self.client.client_state(),
params,
)?);
Ok(PartialTransaction { client: self.client.clone(), inner: tx })
}
}
impl<T, C> TxClient<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// Get the account nonce for a given account ID.
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, ExtrinsicError> {
let block_ref = self
.client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
crate::blocks::get_account_nonce(&self.client, account_id, block_ref.hash())
.await
.map_err(|e| ExtrinsicError::AccountNonceError {
block_hash: block_ref.hash().into(),
account_id: account_id.encode().into(),
reason: e,
})
}
/// Creates a partial transaction, without submitting it. This can then be signed and submitted.
pub async fn create_partial<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
self.create_partial_offline(call, params)
}
/// Creates a partial V4 transaction, without submitting it. This can then be signed and
/// submitted.
///
/// Prefer [`Self::create_partial()`] if you don't know which version to create; this will pick
/// the most suitable one for the given chain.
pub async fn create_v4_partial<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
self.create_v4_partial_offline(call, params)
}
/// Creates a partial V5 transaction, without submitting it. This can then be signed and
/// submitted.
///
/// Prefer [`Self::create_partial()`] if you don't know which version to create; this will pick
/// the most suitable one for the given chain.
pub async fn create_v5_partial<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
self.create_v5_partial_offline(call, params)
}
/// Creates a signed transaction, without submitting it.
pub async fn create_signed<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
{
let mut partial = self.create_partial(call, &signer.account_id(), params).await?;
Ok(partial.sign(signer))
}
/// Creates and signs an transaction and submits it to the chain. Passes default parameters
/// to construct the "signed extra" and "additional" payloads needed by the transaction.
///
/// Returns a [`TxProgress`], 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_default<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
) -> Result<TxProgress<T, C>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
<T::ExtrinsicParams as ExtrinsicParams<T>>::Params: DefaultParams,
{
self.sign_and_submit_then_watch(call, signer, DefaultParams::default_params())
.await
}
/// Creates and signs an transaction and submits it to the chain.
///
/// Returns a [`TxProgress`], 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<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<TxProgress<T, C>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
{
self.create_signed(call, signer, params).await?.submit_and_watch().await
}
/// Creates and signs an transaction and submits to the chain for block inclusion. Passes
/// default parameters to construct the "signed extra" and "additional" payloads needed
/// by the transaction.
///
/// Returns `Ok` with the transaction hash if it is valid transaction.
///
/// # Note
///
/// Success does not mean the transaction has been included in the block, just that it is valid
/// and has been included in the transaction pool.
pub async fn sign_and_submit_default<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
<T::ExtrinsicParams as ExtrinsicParams<T>>::Params: DefaultParams,
{
self.sign_and_submit(call, signer, DefaultParams::default_params()).await
}
/// Creates and signs an transaction and submits to the chain for block inclusion.
///
/// Returns `Ok` with the transaction hash if it is valid transaction.
///
/// # Note
///
/// Success does not mean the transaction has been included in the block, just that it is valid
/// and has been included in the transaction pool.
pub async fn sign_and_submit<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
{
self.create_signed(call, signer, params).await?.submit().await
}
}
/// This payload contains the information needed to produce an transaction.
pub struct PartialTransaction<T: Config, C> {
client: C,
inner: PartialTransactionInner<T>,
}
enum PartialTransactionInner<T: Config> {
V4(pezkuwi_subxt_core::tx::PartialTransactionV4<T>),
V5(pezkuwi_subxt_core::tx::PartialTransactionV5<T>),
}
impl<T, C> PartialTransaction<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
/// Return the signer payload for this transaction. These are the bytes that must
/// be signed in order to produce a valid signature for the transaction.
pub fn signer_payload(&self) -> Vec<u8> {
match &self.inner {
PartialTransactionInner::V4(tx) => tx.signer_payload(),
PartialTransactionInner::V5(tx) => tx.signer_payload().to_vec(),
}
}
/// Return the bytes representing the call data for this partially constructed
/// transaction.
pub fn call_data(&self) -> &[u8] {
match &self.inner {
PartialTransactionInner::V4(tx) => tx.call_data(),
PartialTransactionInner::V5(tx) => tx.call_data(),
}
}
/// Convert this [`PartialTransaction`] into a [`SubmittableTransaction`], ready to submit.
/// The provided `signer` is responsible for providing the "from" address for the transaction,
/// as well as providing a signature to attach to it.
pub fn sign<Signer>(&mut self, signer: &Signer) -> SubmittableTransaction<T, C>
where
Signer: SignerT<T>,
{
let tx = match &mut self.inner {
PartialTransactionInner::V4(tx) => tx.sign(signer),
PartialTransactionInner::V5(tx) => tx.sign(signer),
};
SubmittableTransaction { client: self.client.clone(), inner: tx }
}
/// Convert this [`PartialTransaction`] into a [`SubmittableTransaction`], ready to submit.
/// An address, and something representing a signature that can be SCALE encoded, are both
/// needed in order to construct it. If you have a `Signer` to hand, you can use
/// [`PartialTransaction::sign()`] instead.
pub fn sign_with_account_and_signature(
&mut self,
account_id: &T::AccountId,
signature: &T::Signature,
) -> SubmittableTransaction<T, C> {
let tx = match &mut self.inner {
PartialTransactionInner::V4(tx) =>
tx.sign_with_account_and_signature(account_id.clone(), signature),
PartialTransactionInner::V5(tx) =>
tx.sign_with_account_and_signature(account_id, signature),
};
SubmittableTransaction { client: self.client.clone(), inner: tx }
}
}
/// This represents an transaction that has been signed and is ready to submit.
pub struct SubmittableTransaction<T, C> {
client: C,
inner: pezkuwi_subxt_core::tx::Transaction<T>,
}
impl<T, C> SubmittableTransaction<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
/// Create a [`SubmittableTransaction`] from some already-signed and prepared
/// transaction bytes, and some client (anything implementing [`OfflineClientT`]
/// or [`OnlineClientT`]).
///
/// Prefer to use [`TxClient`] to create and sign transactions. This is simply
/// exposed in case you want to skip this process and submit something you've
/// already created.
pub fn from_bytes(client: C, tx_bytes: Vec<u8>) -> Self {
Self { client, inner: pezkuwi_subxt_core::tx::Transaction::from_bytes(tx_bytes) }
}
/// Calculate and return the hash of the transaction, based on the configured hasher.
pub fn hash(&self) -> HashFor<T> {
self.inner.hash_with(self.client.hasher())
}
/// Returns the SCALE encoded transaction bytes.
pub fn encoded(&self) -> &[u8] {
self.inner.encoded()
}
/// Consumes [`SubmittableTransaction`] and returns the SCALE encoded
/// transaction bytes.
pub fn into_encoded(self) -> Vec<u8> {
self.inner.into_encoded()
}
}
impl<T, C> SubmittableTransaction<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// Submits the transaction to the chain.
///
/// Returns a [`TxProgress`], 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 submit_and_watch(&self) -> Result<TxProgress<T, C>, ExtrinsicError> {
// Get a hash of the transaction (we'll need this later).
let ext_hash = self.hash();
// Submit and watch for transaction progress.
let sub = self
.client
.backend()
.submit_transaction(self.encoded())
.await
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
Ok(TxProgress::new(sub, self.client.clone(), ext_hash))
}
/// Submits the transaction to the chain for block inclusion.
///
/// 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<HashFor<T>, ExtrinsicError> {
let ext_hash = self.hash();
let mut sub = self
.client
.backend()
.submit_transaction(self.encoded())
.await
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
// 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::NoLongerInBestBlock |
TransactionStatus::InFinalizedBlock { .. } => Ok(ext_hash),
TransactionStatus::Error { message } => Err(
ExtrinsicError::TransactionStatusError(TransactionStatusError::Error(message)),
),
TransactionStatus::Invalid { message } =>
Err(ExtrinsicError::TransactionStatusError(TransactionStatusError::Invalid(
message,
))),
TransactionStatus::Dropped { message } =>
Err(ExtrinsicError::TransactionStatusError(TransactionStatusError::Dropped(
message,
))),
},
Some(Err(e)) => Err(ExtrinsicError::TransactionStatusStreamError(e)),
None => Err(ExtrinsicError::UnexpectedEndOfTransactionStatusStream),
}
}
/// 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
/// transaction.
pub async fn validate(&self) -> Result<ValidationResult, ExtrinsicError> {
let latest_block_ref = self
.client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
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
/// transaction.
pub async fn validate_at(
&self,
at: impl Into<BlockRef<HashFor<T>>>,
) -> Result<ValidationResult, ExtrinsicError> {
let block_hash = at.into().hash();
// Approach taken from https://github.com/pezkuwichain/json-rpc-interface-spec/issues/55.
let mut params = Vec::with_capacity(8 + self.encoded().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(&params), block_hash)
.await
.map_err(ExtrinsicError::CannotGetValidationInfo)?;
ValidationResult::try_from_bytes(res)
}
/// This returns an estimate for what the transaction is expected to cost to execute, less any
/// tips. The actual amount paid can vary from block to block based on node traffic and other
/// factors.
pub async fn partial_fee_estimate(&self) -> Result<u128, ExtrinsicError> {
let mut params = self.encoded().to_vec();
(self.encoded().len() as u32).encode_to(&mut params);
let latest_block_ref = self
.client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
// destructuring RuntimeDispatchInfo, see type information <https://pezkuwichain.github.io/bizinikiwi/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
.backend()
.call_decoding::<(Compact<u64>, Compact<u64>, u8, u128)>(
"TransactionPaymentApi_query_info",
Some(&params),
latest_block_ref.hash(),
)
.await
.map_err(ExtrinsicError::CannotGetFeeInfo)?;
Ok(partial_fee)
}
}
/// Fetch the latest block header and account nonce from the backend and use them to refine
/// [`ExtrinsicParams::Params`].
async fn inject_account_nonce_and_block<T: Config, Client: OnlineClientT<T>>(
client: &Client,
account_id: &T::AccountId,
params: &mut <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<(), ExtrinsicError> {
use pezkuwi_subxt_core::config::transaction_extensions::Params;
let block_ref = client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
let (block_header, account_nonce) = try_join(
client
.backend()
.block_header(block_ref.hash())
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock),
crate::blocks::get_account_nonce(client, account_id, block_ref.hash()).map_err(|e| {
ExtrinsicError::AccountNonceError {
block_hash: block_ref.hash().into(),
account_id: account_id.encode().into(),
reason: e,
}
}),
)
.await?;
let block_header = block_header.ok_or_else(|| ExtrinsicError::CannotFindBlockHeader {
block_hash: block_ref.hash().into(),
})?;
params.inject_account_nonce(account_nonce);
params.inject_block(block_header.number().into(), block_ref.hash());
Ok(())
}
impl ValidationResult {
#[allow(clippy::get_first)]
fn try_from_bytes(bytes: Vec<u8>) -> Result<ValidationResult, ExtrinsicError> {
// TaggedTransactionQueue_validate_transaction returns this:
// https://github.com/pezkuwichain/bizinikiwi/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..])
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
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..])
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
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..])
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
Ok(ValidationResult::Unknown(res))
} else {
// unable to decode the bytes; they aren't what we expect.
Err(ExtrinsicError::UnexpectedValidationResultBytes(bytes))
}
}
}
/// The result of performing [`SubmittableTransaction::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 Bizinikiwi 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 Pezkuwi `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 transaction 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 transaction with a mandatory dispatch tried to be validated.
/// This is invalid; only inherent transactions are allowed to have mandatory dispatches.
MandatoryValidation,
/// The sending address is disabled or known to be invalid.
BadSigner,
}
/// This trait is used to create default values for extrinsic params. We use this instead of
/// [`Default`] because we want to be able to support params which are tuples of more than 12
/// entries (which is the maximum tuple size Rust currently implements [`Default`] for on tuples),
/// given that we aren't far off having more than 12 transaction extensions already.
///
/// If you have params which are _not_ a tuple and which you'd like to be instantiated automatically
/// when calling [`TxClient::sign_and_submit_default()`] or
/// [`TxClient::sign_and_submit_then_watch_default()`], then you'll need to implement this trait for
/// them.
pub trait DefaultParams: Sized {
/// Instantiate a default instance of the parameters.
fn default_params() -> Self;
}
impl<const N: usize, P: Default> DefaultParams for [P; N] {
fn default_params() -> Self {
core::array::from_fn(|_| P::default())
}
}
macro_rules! impl_default_params_for_tuple {
($($ident:ident),+) => {
impl <$($ident : Default),+> DefaultParams for ($($ident,)+){
fn default_params() -> Self {
(
$($ident::default(),)+
)
}
}
}
}
#[rustfmt::skip]
const _: () = {
impl_default_params_for_tuple!(A);
impl_default_params_for_tuple!(A, B);
impl_default_params_for_tuple!(A, B, C);
impl_default_params_for_tuple!(A, B, C, D);
impl_default_params_for_tuple!(A, B, C, D, E);
impl_default_params_for_tuple!(A, B, C, D, E, F);
impl_default_params_for_tuple!(A, B, C, D, E, F, G);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z);
};
#[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, 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);
}
}
}
+445
View File
@@ -0,0 +1,445 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types representing extrinsics/transactions that have been submitted to a node.
use std::task::Poll;
use crate::{
backend::{BlockRef, StreamOfResults, TransactionStatus as BackendTxStatus},
client::OnlineClientT,
config::{Config, HashFor},
error::{
DispatchError, TransactionEventsError, TransactionFinalizedSuccessError,
TransactionProgressError, TransactionStatusError,
},
events::EventsClient,
utils::strip_compact_prefix,
};
use derive_where::derive_where;
use futures::{Stream, StreamExt};
/// This struct represents a subscription to the progress of some transaction.
pub struct TxProgress<T: Config, C> {
sub: Option<StreamOfResults<BackendTxStatus<HashFor<T>>>>,
ext_hash: HashFor<T>,
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").
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: StreamOfResults<BackendTxStatus<HashFor<T>>>,
client: C,
ext_hash: HashFor<T>,
) -> Self {
Self { sub: Some(sub), client, ext_hash }
}
/// Return the hash of the extrinsic.
pub fn extrinsic_hash(&self) -> HashFor<T> {
self.ext_hash
}
}
impl<T, C> TxProgress<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// 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(&mut self) -> Option<Result<TxStatus<T, C>, TransactionProgressError>> {
StreamExt::next(self).await
}
/// Wait for the transaction to be finalized, and return a [`TxInBlock`]
/// 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()`] 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>, TransactionProgressError> {
while let Some(status) = self.next().await {
match status? {
// Finalized! Return.
TxStatus::InFinalizedBlock(s) => return Ok(s),
// Error scenarios; return the error.
TxStatus::Error { message } => {
return Err(TransactionStatusError::Error(message).into());
},
TxStatus::Invalid { message } => {
return Err(TransactionStatusError::Invalid(message).into());
},
TxStatus::Dropped { message } => {
return Err(TransactionStatusError::Dropped(message).into());
},
// Ignore and wait for next status event:
_ => continue,
}
}
Err(TransactionProgressError::UnexpectedEndOfTransactionStatusStream)
}
/// 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 [`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_success(
self,
) -> Result<crate::blocks::ExtrinsicEvents<T>, TransactionFinalizedSuccessError> {
let evs = self.wait_for_finalized().await?.wait_for_success().await?;
Ok(evs)
}
}
impl<T: Config, C: Clone> Stream for TxProgress<T, C> {
type Item = Result<TxStatus<T, C>, TransactionProgressError>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let sub = match self.sub.as_mut() {
Some(sub) => sub,
None => return Poll::Ready(None),
};
sub.poll_next_unpin(cx)
.map_err(TransactionProgressError::CannotGetNextProgressUpdate)
.map_ok(|status| {
match status {
BackendTxStatus::Validated => TxStatus::Validated,
BackendTxStatus::Broadcasted => TxStatus::Broadcasted,
BackendTxStatus::NoLongerInBestBlock => TxStatus::NoLongerInBestBlock,
BackendTxStatus::InBestBlock { hash } => TxStatus::InBestBlock(TxInBlock::new(
hash,
self.ext_hash,
self.client.clone(),
)),
// These stream events mean that nothing further will be sent:
BackendTxStatus::InFinalizedBlock { hash } => {
self.sub = None;
TxStatus::InFinalizedBlock(TxInBlock::new(
hash,
self.ext_hash,
self.client.clone(),
))
},
BackendTxStatus::Error { message } => {
self.sub = None;
TxStatus::Error { message }
},
BackendTxStatus::Invalid { message } => {
self.sub = None;
TxStatus::Invalid { message }
},
BackendTxStatus::Dropped { message } => {
self.sub = None;
TxStatus::Dropped { message }
},
}
})
}
}
/// Possible transaction statuses returned from our [`TxProgress::next()`] call.
#[derive_where(Debug; C)]
pub enum TxStatus<T: Config, C> {
/// Transaction is part of the future queue.
Validated,
/// The transaction has been broadcast to other nodes.
Broadcasted,
/// Transaction is no longer in a best block.
NoLongerInBestBlock,
/// 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::InFinalizedBlock`].
pub fn as_finalized(&self) -> Option<&TxInBlock<T, C>> {
match self {
Self::InFinalizedBlock(val) => Some(val),
_ => None,
}
}
/// 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::InBestBlock(val) => Some(val),
_ => None,
}
}
}
/// This struct represents a transaction that has made it into a block.
#[derive_where(Debug; C)]
pub struct TxInBlock<T: Config, C> {
block_ref: BlockRef<HashFor<T>>,
ext_hash: HashFor<T>,
client: C,
}
impl<T: Config, C> TxInBlock<T, C> {
pub(crate) fn new(block_ref: BlockRef<HashFor<T>>, ext_hash: HashFor<T>, client: C) -> Self {
Self { block_ref, ext_hash, client }
}
/// Return the hash of the block that the transaction has made it into.
pub fn block_hash(&self) -> HashFor<T> {
self.block_ref.hash()
}
/// Return the hash of the extrinsic that was submitted.
pub fn extrinsic_hash(&self) -> HashFor<T> {
self.ext_hash
}
}
impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
/// 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 [`TxInBlock::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<crate::blocks::ExtrinsicEvents<T>, TransactionEventsError> {
let events = self.fetch_events().await?;
// Try to find any errors; return the first one we encounter.
for (ev_idx, ev) in events.iter().enumerate() {
let ev = ev.map_err(|e| TransactionEventsError::CannotDecodeEventInBlock {
event_index: ev_idx,
block_hash: self.block_hash().into(),
error: e,
})?;
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
let dispatch_error =
DispatchError::decode_from(ev.field_bytes(), self.client.metadata()).map_err(
|e| TransactionEventsError::CannotDecodeDispatchError {
error: e,
bytes: ev.field_bytes().to_vec(),
},
)?;
return Err(dispatch_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<crate::blocks::ExtrinsicEvents<T>, TransactionEventsError> {
let hasher = self.client.hasher();
let block_body = self
.client
.backend()
.block_body(self.block_ref.hash())
.await
.map_err(|e| TransactionEventsError::CannotFetchBlockBody {
block_hash: self.block_hash().into(),
error: e,
})?
.ok_or_else(|| TransactionEventsError::BlockNotFound {
block_hash: self.block_hash().into(),
})?;
let extrinsic_idx = block_body
.iter()
.position(|ext| {
use crate::config::Hasher;
let Ok((_, stripped)) = strip_compact_prefix(ext) else {
return false;
};
let hash = hasher.hash_of(&stripped);
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_else(|| TransactionEventsError::CannotFindTransactionInBlock {
block_hash: self.block_hash().into(),
transaction_hash: self.ext_hash.into(),
})?;
let events = EventsClient::new(self.client.clone())
.at(self.block_ref.clone())
.await
.map_err(|e| TransactionEventsError::CannotFetchEventsForTransaction {
block_hash: self.block_hash().into(),
transaction_hash: self.ext_hash.into(),
error: e,
})?;
Ok(crate::blocks::ExtrinsicEvents::new(self.ext_hash, extrinsic_idx as u32, events))
}
}
#[cfg(test)]
mod test {
use super::*;
use pezkuwi_subxt_core::client::RuntimeVersion;
use crate::{
BizinikiwConfig,
backend::{StreamOfResults, TransactionStatus},
client::{OfflineClientT, OnlineClientT},
config::{Config, HashFor},
tx::TxProgress,
};
type MockTxProgress = TxProgress<BizinikiwConfig, MockClient>;
type MockHash = HashFor<BizinikiwConfig>;
type MockBizinikiwiTxStatus = TransactionStatus<MockHash>;
/// a mock client to satisfy trait bounds in tests
#[derive(Clone, Debug)]
struct MockClient;
impl OfflineClientT<BizinikiwConfig> for MockClient {
fn metadata(&self) -> crate::Metadata {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn genesis_hash(&self) -> MockHash {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn runtime_version(&self) -> RuntimeVersion {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn hasher(&self) -> <BizinikiwConfig as Config>::Hasher {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn client_state(&self) -> pezkuwi_subxt_core::client::ClientState<BizinikiwConfig> {
unimplemented!("just a mock impl to satisfy trait bounds")
}
}
impl OnlineClientT<BizinikiwConfig> for MockClient {
fn backend(&self) -> &dyn crate::backend::Backend<BizinikiwConfig> {
unimplemented!("just a mock impl to satisfy trait bounds")
}
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_error() {
let tx_progress = mock_tx_progress(vec![
MockBizinikiwiTxStatus::Broadcasted,
MockBizinikiwiTxStatus::Error { message: "err".into() },
]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Error(e))) if e == "err"
));
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_invalid() {
let tx_progress = mock_tx_progress(vec![
MockBizinikiwiTxStatus::Broadcasted,
MockBizinikiwiTxStatus::Invalid { message: "err".into() },
]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Invalid(e))) if e == "err"
));
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_dropped() {
let tx_progress = mock_tx_progress(vec![
MockBizinikiwiTxStatus::Broadcasted,
MockBizinikiwiTxStatus::Dropped { message: "err".into() },
]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Dropped(e))) if e == "err"
));
}
fn mock_tx_progress(statuses: Vec<MockBizinikiwiTxStatus>) -> MockTxProgress {
let sub = create_bizinikiwi_tx_status_subscription(statuses);
TxProgress::new(sub, MockClient, Default::default())
}
fn create_bizinikiwi_tx_status_subscription(
elements: Vec<MockBizinikiwiTxStatus>,
) -> StreamOfResults<MockBizinikiwiTxStatus> {
let results = elements.into_iter().map(Ok);
let stream = Box::pin(futures::stream::iter(results));
let sub: StreamOfResults<MockBizinikiwiTxStatus> = StreamOfResults::new(stream);
sub
}
}