mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-18 23:31:04 +00:00
Rework Subxt API to support offline and dynamic transactions (#593)
* WIP API changes * debug impls * Get main crate compiling with first round of changes * Some tidy up * Add WithExtrinsicParams, and have SubstrateConfig + PolkadotConfig, not DefaultConfig * move transaction into extrinsic folder * Add runtime updates back to OnlineClient * rework to be 'client first' to fit better with storage + events * add support for events to Client * tidy dupe trait bound * Wire storage into client, but need to remove static reliance * various tidy up and start stripping codegen to remove bits we dont need now * First pass updating calls and constants codegen * WIP storage client updates * First pass migrated runtime storage over to new format * pass over codegen to generate StorageAddresses and throw other stuff out * don't need a Call trait any more * shuffle things around a bit * Various proc_macro fixes to get 'cargo check' working * organise what's exposed from subxt * Get first example working; balance_transfer_with_params * get balance_transfer example compiling * get concurrent_storage_requests.rs example compiling * get fetch_all_accounts example compiling * get a bunch more of the examples compiling * almost get final example working; type mismatch to look into * wee tweaks * move StorageAddress to separate file * pass Defaultable/Iterable info to StorageAddress in codegen * fix storage validation ne, and partial run through example code * Remove static iteration and strip a generic param from everything * fix doc tests in subxt crate * update test utils and start fixing frame tests * fix frame staking tests * fix the rest of the test compile issues, Borrow on storage values * cargo fmt * remove extra logging during tests * Appease clippy and no more need for into_iter on events * cargo fmt * fix dryRun tests by waiting for blocks * wait for blocks instead of sleeping or other test hacks * cargo fmt * Fix doc links * Traitify StorageAddress * remove out-of-date doc comments * optimise decoding storage a little * cleanup tx stuff, trait for TxPayload, remove Err type param and decode at runtime * clippy fixes * fix doc links * fix doc example * constant address trait for consistency * fix a typo and remove EncodeWithMetadata stuff * Put EventDetails behind a proper interface and allow decoding into top level event, too * fix docs * tweak StorageAddress docs * re-export StorageAddress at root for consistency * fix clippy things * Add support for dynamic values * fix double encoding of storage map key after refactor * clippy fix * Fixes and add a dynamic usage example (needs new scale_value release) * bump scale_value version * cargo fmt * Tweak event bits * cargo fmt * Add a test and bump scale-value to 0.4.0 to support this * remove unnecessary vec from dynamic example * Various typo/grammar fixes Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> * Address PR nits * Undo accidental rename in changelog * Small PR nits/tidyups * fix tests; codegen change against latest substrate * tweak storage address util names * move error decoding to DecodeError and expose * impl some basic traits on the extrinsic param builder Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Create signed or unsigned extrinsics.
|
||||
//!
|
||||
//! This modules exposes the extrinsic's parameters and the ability to sign an extrinsic.
|
||||
//!
|
||||
//!
|
||||
//! An extrinsic is submitted with an "signed extra" and "additional" parameters, which can be
|
||||
//! different for each chain. The trait [ExtrinsicParams] determines exactly which
|
||||
//! additional and signed extra parameters are used when constructing an extrinsic.
|
||||
//!
|
||||
//!
|
||||
//! The structure [BaseExtrinsicParams] is a base implementation of the trait which
|
||||
//! configures most of the "signed extra" and "additional" parameters as needed for
|
||||
//! Polkadot and Substrate nodes. Only the shape of the tip payments differs, leading to
|
||||
//! [SubstrateExtrinsicParams] and [PolkadotExtrinsicParams] structs which pick an
|
||||
//! appropriate shape for Substrate/Polkadot chains respectively.
|
||||
|
||||
mod params;
|
||||
mod signer;
|
||||
mod tx_client;
|
||||
mod tx_payload;
|
||||
mod tx_progress;
|
||||
|
||||
pub use self::{
|
||||
params::{
|
||||
AssetTip,
|
||||
BaseExtrinsicParams,
|
||||
BaseExtrinsicParamsBuilder,
|
||||
Era,
|
||||
ExtrinsicParams,
|
||||
PlainTip,
|
||||
PolkadotExtrinsicParams,
|
||||
PolkadotExtrinsicParamsBuilder,
|
||||
SubstrateExtrinsicParams,
|
||||
SubstrateExtrinsicParamsBuilder,
|
||||
},
|
||||
signer::{
|
||||
PairSigner,
|
||||
Signer,
|
||||
},
|
||||
tx_client::{
|
||||
SignedSubmittableExtrinsic,
|
||||
TxClient,
|
||||
},
|
||||
tx_payload::{
|
||||
dynamic,
|
||||
DynamicTxPayload,
|
||||
StaticTxPayload,
|
||||
TxPayload,
|
||||
},
|
||||
tx_progress::{
|
||||
TxEvents,
|
||||
TxInBlock,
|
||||
TxProgress,
|
||||
TxStatus,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::{
|
||||
utils::Encoded,
|
||||
Config,
|
||||
};
|
||||
use codec::{
|
||||
Compact,
|
||||
Encode,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use derivative::Derivative;
|
||||
|
||||
// We require Era as a param below, so make it available from here.
|
||||
pub use sp_runtime::generic::Era;
|
||||
|
||||
/// This trait allows you to configure the "signed extra" and
|
||||
/// "additional" parameters that are signed and used in transactions.
|
||||
/// see [`BaseExtrinsicParams`] for an implementation that is compatible with
|
||||
/// a Polkadot node.
|
||||
pub trait ExtrinsicParams<Index, Hash>: Debug + 'static {
|
||||
/// These parameters can be provided to the constructor along with
|
||||
/// some default parameters that `subxt` understands, in order to
|
||||
/// help construct your [`ExtrinsicParams`] object.
|
||||
type OtherParams;
|
||||
|
||||
/// Construct a new instance of our [`ExtrinsicParams`]
|
||||
fn new(
|
||||
spec_version: u32,
|
||||
tx_version: u32,
|
||||
nonce: Index,
|
||||
genesis_hash: Hash,
|
||||
other_params: Self::OtherParams,
|
||||
) -> Self;
|
||||
|
||||
/// This is expected to SCALE encode the "signed extra" parameters
|
||||
/// to some buffer that has been provided. These are the parameters
|
||||
/// which are sent along with the transaction, as well as taken into
|
||||
/// account when signing the transaction.
|
||||
fn encode_extra_to(&self, v: &mut Vec<u8>);
|
||||
|
||||
/// This is expected to SCALE encode the "additional" parameters
|
||||
/// to some buffer that has been provided. These parameters are _not_
|
||||
/// sent along with the transaction, but are taken into account when
|
||||
/// signing it, meaning the client and node must agree on their values.
|
||||
fn encode_additional_to(&self, v: &mut Vec<u8>);
|
||||
}
|
||||
|
||||
/// A struct representing the signed extra and additional parameters required
|
||||
/// to construct a transaction for the default substrate node.
|
||||
pub type SubstrateExtrinsicParams<T> = BaseExtrinsicParams<T, AssetTip>;
|
||||
|
||||
/// A builder which leads to [`SubstrateExtrinsicParams`] being constructed.
|
||||
/// This is what you provide to methods like `sign_and_submit()`.
|
||||
pub type SubstrateExtrinsicParamsBuilder<T> = BaseExtrinsicParamsBuilder<T, AssetTip>;
|
||||
|
||||
/// A struct representing the signed extra and additional parameters required
|
||||
/// to construct a transaction for a polkadot node.
|
||||
pub type PolkadotExtrinsicParams<T> = BaseExtrinsicParams<T, PlainTip>;
|
||||
|
||||
/// A builder which leads to [`PolkadotExtrinsicParams`] being constructed.
|
||||
/// This is what you provide to methods like `sign_and_submit()`.
|
||||
pub type PolkadotExtrinsicParamsBuilder<T> = BaseExtrinsicParamsBuilder<T, PlainTip>;
|
||||
|
||||
/// An implementation of [`ExtrinsicParams`] that is suitable for constructing
|
||||
/// extrinsics that can be sent to a node with the same signed extra and additional
|
||||
/// parameters as a Polkadot/Substrate node. The way that tip payments are specified
|
||||
/// differs between Substrate and Polkadot nodes, and so we are generic over that in
|
||||
/// order to support both here with relative ease.
|
||||
///
|
||||
/// If your node differs in the "signed extra" and "additional" parameters expected
|
||||
/// to be sent/signed with a transaction, then you can define your own type which
|
||||
/// implements the [`ExtrinsicParams`] trait.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = "Tip: Debug"))]
|
||||
pub struct BaseExtrinsicParams<T: Config, Tip: Debug> {
|
||||
era: Era,
|
||||
nonce: T::Index,
|
||||
tip: Tip,
|
||||
spec_version: u32,
|
||||
transaction_version: u32,
|
||||
genesis_hash: T::Hash,
|
||||
mortality_checkpoint: T::Hash,
|
||||
marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
/// This builder allows you to provide the parameters that can be configured in order to
|
||||
/// construct a [`BaseExtrinsicParams`] value. This implements [`Default`], which allows
|
||||
/// [`BaseExtrinsicParams`] to be used with convenience methods like `sign_and_submit_default()`.
|
||||
///
|
||||
/// Prefer to use [`SubstrateExtrinsicParamsBuilder`] for a version of this tailored towards
|
||||
/// Substrate, or [`PolkadotExtrinsicParamsBuilder`] for a version tailored to Polkadot.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(
|
||||
Debug(bound = "Tip: Debug"),
|
||||
Clone(bound = "Tip: Clone"),
|
||||
Copy(bound = "Tip: Copy"),
|
||||
PartialEq(bound = "Tip: PartialEq")
|
||||
)]
|
||||
pub struct BaseExtrinsicParamsBuilder<T: Config, Tip> {
|
||||
era: Era,
|
||||
mortality_checkpoint: Option<T::Hash>,
|
||||
tip: Tip,
|
||||
}
|
||||
|
||||
impl<T: Config, Tip: Default> BaseExtrinsicParamsBuilder<T, Tip> {
|
||||
/// Instantiate the default set of [`BaseExtrinsicParamsBuilder`]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the [`Era`], which defines how long the transaction will be valid for
|
||||
/// (it can be either immortal, or it can be mortal and expire after a certain amount
|
||||
/// of time). The second argument is the block hash after which the transaction
|
||||
/// becomes valid, and must align with the era phase (see the [`Era::Mortal`] docs
|
||||
/// for more detail on that).
|
||||
pub fn era(mut self, era: Era, checkpoint: T::Hash) -> Self {
|
||||
self.era = era;
|
||||
self.mortality_checkpoint = Some(checkpoint);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tip you'd like to give to the block author
|
||||
/// for this transaction.
|
||||
pub fn tip(mut self, tip: impl Into<Tip>) -> Self {
|
||||
self.tip = tip.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, Tip: Default> Default for BaseExtrinsicParamsBuilder<T, Tip> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
era: Era::Immortal,
|
||||
mortality_checkpoint: None,
|
||||
tip: Tip::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, Tip: Debug + Encode + 'static> ExtrinsicParams<T::Index, T::Hash>
|
||||
for BaseExtrinsicParams<T, Tip>
|
||||
{
|
||||
type OtherParams = BaseExtrinsicParamsBuilder<T, Tip>;
|
||||
|
||||
fn new(
|
||||
// Provided from subxt client:
|
||||
spec_version: u32,
|
||||
transaction_version: u32,
|
||||
nonce: T::Index,
|
||||
genesis_hash: T::Hash,
|
||||
// Provided externally:
|
||||
other_params: Self::OtherParams,
|
||||
) -> Self {
|
||||
BaseExtrinsicParams {
|
||||
era: other_params.era,
|
||||
mortality_checkpoint: other_params
|
||||
.mortality_checkpoint
|
||||
.unwrap_or(genesis_hash),
|
||||
tip: other_params.tip,
|
||||
nonce,
|
||||
spec_version,
|
||||
transaction_version,
|
||||
genesis_hash,
|
||||
marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_extra_to(&self, v: &mut Vec<u8>) {
|
||||
let nonce: u64 = self.nonce.into();
|
||||
let tip = Encoded(self.tip.encode());
|
||||
(self.era, Compact(nonce), tip).encode_to(v);
|
||||
}
|
||||
|
||||
fn encode_additional_to(&self, v: &mut Vec<u8>) {
|
||||
(
|
||||
self.spec_version,
|
||||
self.transaction_version,
|
||||
self.genesis_hash,
|
||||
self.mortality_checkpoint,
|
||||
)
|
||||
.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
/// A tip payment.
|
||||
#[derive(Copy, Clone, Debug, Default, Encode)]
|
||||
pub struct PlainTip {
|
||||
#[codec(compact)]
|
||||
tip: u128,
|
||||
}
|
||||
|
||||
impl PlainTip {
|
||||
/// Create a new tip of the amount provided.
|
||||
pub fn new(amount: u128) -> Self {
|
||||
PlainTip { tip: amount }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for PlainTip {
|
||||
fn from(n: u128) -> Self {
|
||||
PlainTip::new(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// A tip payment made in the form of a specific asset.
|
||||
#[derive(Copy, Clone, Debug, Default, Encode)]
|
||||
pub struct AssetTip {
|
||||
#[codec(compact)]
|
||||
tip: u128,
|
||||
asset: Option<u32>,
|
||||
}
|
||||
|
||||
impl AssetTip {
|
||||
/// Create a new tip of the amount provided.
|
||||
pub fn new(amount: u128) -> Self {
|
||||
AssetTip {
|
||||
tip: amount,
|
||||
asset: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Designate the tip as being of a particular asset class.
|
||||
/// If this is not set, then the native currency is used.
|
||||
pub fn of_asset(mut self, asset: u32) -> Self {
|
||||
self.asset = Some(asset);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for AssetTip {
|
||||
fn from(n: u128) -> Self {
|
||||
AssetTip::new(n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! A library to **sub**mit e**xt**rinsics to a
|
||||
//! [substrate](https://github.com/paritytech/substrate) node via RPC.
|
||||
|
||||
use crate::Config;
|
||||
use sp_core::Pair;
|
||||
use sp_runtime::traits::{
|
||||
IdentifyAccount,
|
||||
Verify,
|
||||
};
|
||||
|
||||
/// Signing transactions requires a [`Signer`]. This is responsible for
|
||||
/// providing the "from" account that the transaction is being signed by,
|
||||
/// as well as actually signing a SCALE encoded payload. Optionally, a
|
||||
/// signer can also provide the nonce for the transaction to use.
|
||||
pub trait Signer<T: Config> {
|
||||
/// Optionally returns a nonce.
|
||||
fn nonce(&self) -> Option<T::Index>;
|
||||
|
||||
/// Return the "from" account ID.
|
||||
fn account_id(&self) -> &T::AccountId;
|
||||
|
||||
/// Return the "from" address.
|
||||
fn address(&self) -> T::Address;
|
||||
|
||||
/// Takes a signer payload for an extrinsic, and returns a signature based on it.
|
||||
///
|
||||
/// Some signers may fail, for instance because the hardware on which the keys are located has
|
||||
/// refused the operation.
|
||||
fn sign(&self, signer_payload: &[u8]) -> T::Signature;
|
||||
}
|
||||
|
||||
/// A [`Signer`] implementation that can be constructed from an [`Pair`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PairSigner<T: Config, P: Pair> {
|
||||
account_id: T::AccountId,
|
||||
nonce: Option<T::Index>,
|
||||
signer: P,
|
||||
}
|
||||
|
||||
impl<T, P> PairSigner<T, P>
|
||||
where
|
||||
T: Config,
|
||||
T::Signature: From<P::Signature>,
|
||||
<T::Signature as Verify>::Signer:
|
||||
From<P::Public> + IdentifyAccount<AccountId = T::AccountId>,
|
||||
P: Pair,
|
||||
{
|
||||
/// Creates a new [`Signer`] from a [`Pair`].
|
||||
pub fn new(signer: P) -> Self {
|
||||
let account_id =
|
||||
<T::Signature as Verify>::Signer::from(signer.public()).into_account();
|
||||
Self {
|
||||
account_id,
|
||||
nonce: None,
|
||||
signer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the nonce to a new value. By default, the nonce will
|
||||
/// be retrieved from the node. Setting one here will override that.
|
||||
pub fn set_nonce(&mut self, nonce: T::Index) {
|
||||
self.nonce = Some(nonce);
|
||||
}
|
||||
|
||||
/// Increment the nonce.
|
||||
pub fn increment_nonce(&mut self) {
|
||||
self.nonce = self.nonce.map(|nonce| nonce + 1u32.into());
|
||||
}
|
||||
|
||||
/// Returns the [`Pair`] implementation used to construct this.
|
||||
pub fn signer(&self) -> &P {
|
||||
&self.signer
|
||||
}
|
||||
|
||||
/// Return the account ID.
|
||||
pub fn account_id(&self) -> &T::AccountId {
|
||||
&self.account_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, P> Signer<T> for PairSigner<T, P>
|
||||
where
|
||||
T: Config,
|
||||
T::AccountId: Into<T::Address> + Clone + 'static,
|
||||
P: Pair + 'static,
|
||||
P::Signature: Into<T::Signature> + 'static,
|
||||
{
|
||||
fn nonce(&self) -> Option<T::Index> {
|
||||
self.nonce
|
||||
}
|
||||
|
||||
fn account_id(&self) -> &T::AccountId {
|
||||
&self.account_id
|
||||
}
|
||||
|
||||
fn address(&self) -> T::Address {
|
||||
self.account_id.clone().into()
|
||||
}
|
||||
|
||||
fn sign(&self, signer_payload: &[u8]) -> T::Signature {
|
||||
self.signer.sign(signer_payload).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::TxPayload;
|
||||
use crate::{
|
||||
client::{
|
||||
OfflineClientT,
|
||||
OnlineClientT,
|
||||
},
|
||||
error::Error,
|
||||
tx::{
|
||||
ExtrinsicParams,
|
||||
Signer,
|
||||
TxProgress,
|
||||
},
|
||||
utils::{
|
||||
Encoded,
|
||||
PhantomDataSendSync,
|
||||
},
|
||||
Config,
|
||||
};
|
||||
use codec::{
|
||||
Compact,
|
||||
Encode,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use sp_runtime::{
|
||||
traits::Hash,
|
||||
ApplyExtrinsicResult,
|
||||
};
|
||||
|
||||
/// A client for working with transactions.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = "Client: Clone"))]
|
||||
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 extrinsic 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<(), Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
if let Some(actual_hash) = call.validation_hash() {
|
||||
let metadata = self.client.metadata();
|
||||
let expected_hash =
|
||||
metadata.call_hash(call.pallet_name(), call.call_name())?;
|
||||
if actual_hash != expected_hash {
|
||||
return Err(crate::metadata::MetadataError::IncompatibleMetadata.into())
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the SCALE encoded bytes representing the call data of the transaction.
|
||||
pub fn call_data<Call>(&self, call: &Call) -> Result<Vec<u8>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
let metadata = self.client.metadata();
|
||||
let mut bytes = Vec::new();
|
||||
call.encode_call_data(&metadata, &mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Creates a raw signed extrinsic, without submitting it.
|
||||
pub async fn create_signed_with_nonce<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
account_nonce: T::Index,
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<SignedSubmittableExtrinsic<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
self.validate(call)?;
|
||||
|
||||
// 2. SCALE encode call data to bytes (pallet u8, call u8, call params).
|
||||
let call_data = Encoded(self.call_data(call)?);
|
||||
|
||||
// 3. Construct our custom additional/extra params.
|
||||
let additional_and_extra_params = {
|
||||
// Obtain spec version and transaction version from the runtime version of the client.
|
||||
let runtime = self.client.runtime_version();
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::new(
|
||||
runtime.spec_version,
|
||||
runtime.transaction_version,
|
||||
account_nonce,
|
||||
self.client.genesis_hash(),
|
||||
other_params,
|
||||
)
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"tx additional_and_extra_params: {:?}",
|
||||
additional_and_extra_params
|
||||
);
|
||||
|
||||
// 4. Construct signature. This is compatible with the Encode impl
|
||||
// for SignedPayload (which is this payload of bytes that we'd like)
|
||||
// to sign. See:
|
||||
// https://github.com/paritytech/substrate/blob/9a6d706d8db00abb6ba183839ec98ecd9924b1f8/primitives/runtime/src/generic/unchecked_extrinsic.rs#L215)
|
||||
let signature = {
|
||||
let mut bytes = Vec::new();
|
||||
call_data.encode_to(&mut bytes);
|
||||
additional_and_extra_params.encode_extra_to(&mut bytes);
|
||||
additional_and_extra_params.encode_additional_to(&mut bytes);
|
||||
if bytes.len() > 256 {
|
||||
signer.sign(&sp_core::blake2_256(&bytes))
|
||||
} else {
|
||||
signer.sign(&bytes)
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("tx signature: {}", hex::encode(signature.encode()));
|
||||
|
||||
// 5. Encode extrinsic, now that we have the parts we need. This is compatible
|
||||
// with the Encode impl for UncheckedExtrinsic (protocol version 4).
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// "is signed" + transaction protocol version (4)
|
||||
(0b10000000 + 4u8).encode_to(&mut encoded_inner);
|
||||
// from address for signature
|
||||
signer.address().encode_to(&mut encoded_inner);
|
||||
// the signature bytes
|
||||
signature.encode_to(&mut encoded_inner);
|
||||
// attach custom extra params
|
||||
additional_and_extra_params.encode_extra_to(&mut encoded_inner);
|
||||
// and now, call data
|
||||
call_data.encode_to(&mut encoded_inner);
|
||||
// now, prefix byte length:
|
||||
let len = Compact(
|
||||
u32::try_from(encoded_inner.len())
|
||||
.expect("extrinsic size expected to be <4GB"),
|
||||
);
|
||||
let mut encoded = Vec::new();
|
||||
len.encode_to(&mut encoded);
|
||||
encoded.extend(encoded_inner);
|
||||
encoded
|
||||
};
|
||||
|
||||
// Wrap in Encoded to ensure that any more "encode" calls leave it in the right state.
|
||||
// maybe we can just return the raw bytes..
|
||||
Ok(SignedSubmittableExtrinsic {
|
||||
client: self.client.clone(),
|
||||
encoded: Encoded(extrinsic),
|
||||
marker: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OnlineClientT<T>> TxClient<T, C> {
|
||||
/// Creates a raw signed extrinsic, without submitting it.
|
||||
pub async fn create_signed<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<SignedSubmittableExtrinsic<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
// Get nonce from the node.
|
||||
let account_nonce = if let Some(nonce) = signer.nonce() {
|
||||
nonce
|
||||
} else {
|
||||
self.client
|
||||
.rpc()
|
||||
.system_account_next_index(signer.account_id())
|
||||
.await?
|
||||
};
|
||||
|
||||
self.create_signed_with_nonce(call, signer, account_nonce, other_params)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits it to the chain. Passes default parameters
|
||||
/// to construct the "signed extra" and "additional" payloads needed by the extrinsic.
|
||||
///
|
||||
/// 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>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
) -> Result<TxProgress<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams: Default,
|
||||
{
|
||||
self.sign_and_submit_then_watch(call, signer, Default::default())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic 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>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<TxProgress<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
self.create_signed(call, signer, other_params)
|
||||
.await?
|
||||
.submit_and_watch()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits to the chain for block inclusion. Passes
|
||||
/// default parameters to construct the "signed extra" and "additional" payloads needed
|
||||
/// by the extrinsic.
|
||||
///
|
||||
/// 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.
|
||||
pub async fn sign_and_submit_default<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
) -> Result<T::Hash, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams: Default,
|
||||
{
|
||||
self.sign_and_submit(call, signer, Default::default()).await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits 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.
|
||||
pub async fn sign_and_submit<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<T::Hash, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
self.create_signed(call, signer, other_params)
|
||||
.await?
|
||||
.submit()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents an extrinsic that has been signed and is ready to submit.
|
||||
pub struct SignedSubmittableExtrinsic<T, C> {
|
||||
client: C,
|
||||
encoded: Encoded,
|
||||
marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, C> SignedSubmittableExtrinsic<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
/// Submits the extrinsic 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>, Error> {
|
||||
// Get a hash of the extrinsic (we'll need this later).
|
||||
let ext_hash = T::Hashing::hash_of(&self.encoded);
|
||||
|
||||
// Submit and watch for transaction progress.
|
||||
let sub = self.client.rpc().watch_extrinsic(&self.encoded).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.
|
||||
pub async fn submit(&self) -> Result<T::Hash, Error> {
|
||||
self.client.rpc().submit_extrinsic(&self.encoded).await
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the dry_run RPC, to test if it would succeed.
|
||||
///
|
||||
/// Returns `Ok` with an [`ApplyExtrinsicResult`], which is the result of applying of an extrinsic.
|
||||
pub async fn dry_run(
|
||||
&self,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<ApplyExtrinsicResult, Error> {
|
||||
self.client.rpc().dry_run(self.encoded(), at).await
|
||||
}
|
||||
|
||||
/// Returns the SCALE encoded extrinsic bytes.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.encoded.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module contains the trait and types used to represent
|
||||
//! transactions that can be submitted.
|
||||
|
||||
use crate::{
|
||||
dynamic::Value,
|
||||
error::{
|
||||
Error,
|
||||
MetadataError,
|
||||
},
|
||||
metadata::Metadata,
|
||||
};
|
||||
use codec::Encode;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// This represents a transaction payload that can be submitted
|
||||
/// to a node.
|
||||
pub trait TxPayload {
|
||||
/// The name of the pallet that the call lives under.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The name of the call.
|
||||
fn call_name(&self) -> &str;
|
||||
|
||||
/// Encode call data to the provided output.
|
||||
fn encode_call_data(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// An optional validation hash that can be provided
|
||||
/// to verify that the shape of the call on the node
|
||||
/// aligns with our expectations.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a statically generated transaction payload.
|
||||
pub struct StaticTxPayload<CallData> {
|
||||
pallet_name: &'static str,
|
||||
call_name: &'static str,
|
||||
call_data: CallData,
|
||||
validation_hash: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl<CallData> StaticTxPayload<CallData> {
|
||||
/// Create a new [`StaticTxPayload`] from static data.
|
||||
pub fn new(
|
||||
pallet_name: &'static str,
|
||||
call_name: &'static str,
|
||||
call_data: CallData,
|
||||
validation_hash: [u8; 32],
|
||||
) -> Self {
|
||||
StaticTxPayload {
|
||||
pallet_name,
|
||||
call_name,
|
||||
call_data,
|
||||
validation_hash: Some(validation_hash),
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this call prior to submitting it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
validation_hash: None,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CallData: Encode> TxPayload for StaticTxPayload<CallData> {
|
||||
fn pallet_name(&self) -> &str {
|
||||
self.pallet_name
|
||||
}
|
||||
|
||||
fn call_name(&self) -> &str {
|
||||
self.call_name
|
||||
}
|
||||
|
||||
fn encode_call_data(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let pallet = metadata.pallet(self.pallet_name)?;
|
||||
let pallet_index = pallet.index();
|
||||
let call_index = pallet.call_index(self.call_name)?;
|
||||
|
||||
pallet_index.encode_to(out);
|
||||
call_index.encode_to(out);
|
||||
self.call_data.encode_to(out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.validation_hash
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a dynamically generated transaction payload.
|
||||
pub struct DynamicTxPayload<'a> {
|
||||
pallet_name: Cow<'a, str>,
|
||||
call_name: Cow<'a, str>,
|
||||
fields: Vec<Value<()>>,
|
||||
}
|
||||
|
||||
/// Construct a new dynamic transaction payload to submit to a node.
|
||||
pub fn dynamic<'a>(
|
||||
pallet_name: impl Into<Cow<'a, str>>,
|
||||
call_name: impl Into<Cow<'a, str>>,
|
||||
fields: Vec<Value<()>>,
|
||||
) -> DynamicTxPayload<'a> {
|
||||
DynamicTxPayload {
|
||||
pallet_name: pallet_name.into(),
|
||||
call_name: call_name.into(),
|
||||
fields,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TxPayload for DynamicTxPayload<'a> {
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn call_name(&self) -> &str {
|
||||
&self.call_name
|
||||
}
|
||||
|
||||
fn encode_call_data(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let pallet = metadata.pallet(&self.pallet_name)?;
|
||||
let call_id = pallet.call_ty_id().ok_or(MetadataError::CallNotFound)?;
|
||||
let call_value =
|
||||
Value::unnamed_variant(self.call_name.to_owned(), self.fields.clone());
|
||||
|
||||
pallet.index().encode_to(out);
|
||||
scale_value::scale::encode_as_type(&call_value, call_id, metadata.types(), out)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
// Copyright 2019-2022 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::{
|
||||
client::OnlineClientT,
|
||||
error::{
|
||||
DispatchError,
|
||||
Error,
|
||||
TransactionError,
|
||||
},
|
||||
events::{
|
||||
self,
|
||||
EventDetails,
|
||||
Events,
|
||||
EventsClient,
|
||||
Phase,
|
||||
StaticEvent,
|
||||
},
|
||||
rpc::SubstrateTxStatus,
|
||||
Config,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use futures::{
|
||||
Stream,
|
||||
StreamExt,
|
||||
};
|
||||
use jsonrpsee::core::{
|
||||
client::Subscription as RpcSubscription,
|
||||
Error as RpcError,
|
||||
};
|
||||
use sp_runtime::traits::Hash;
|
||||
|
||||
pub use sp_runtime::traits::SignedExtension;
|
||||
|
||||
/// 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<RpcSubscription<SubstrateTxStatus<T::Hash, T::Hash>>>,
|
||||
ext_hash: T::Hash,
|
||||
client: C,
|
||||
}
|
||||
|
||||
// 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: RpcSubscription<SubstrateTxStatus<T::Hash, T::Hash>>,
|
||||
client: C,
|
||||
ext_hash: T::Hash,
|
||||
) -> Self {
|
||||
Self {
|
||||
sub: Some(sub),
|
||||
client,
|
||||
ext_hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OnlineClientT<T>> TxProgress<T, C> {
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be in a block (but not necessarily finalized), and return
|
||||
/// an [`TxInBlock`] instance when this happens, or an error if there was a problem
|
||||
/// waiting for this to happen.
|
||||
///
|
||||
/// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the
|
||||
/// transaction progresses, use [`TxProgress::next_item()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they
|
||||
/// may well indicate with some probability that the transaction will not make it into a block,
|
||||
/// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower
|
||||
/// level [`TxProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_in_block(mut self) -> Result<TxInBlock<T, C>, Error> {
|
||||
while let Some(status) = self.next_item().await {
|
||||
match status? {
|
||||
// Finalized or otherwise in a block! Return.
|
||||
TxStatus::InBlock(s) | TxStatus::Finalized(s) => return Ok(s),
|
||||
// Error scenarios; return the error.
|
||||
TxStatus::FinalityTimeout(_) => {
|
||||
return Err(TransactionError::FinalitySubscriptionTimeout.into())
|
||||
}
|
||||
// Ignore anything else and wait for next status event:
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Err(RpcError::Custom("RPC subscription dropped".into()).into())
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be finalized, and return a [`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_item()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they
|
||||
/// may well indicate with some probability that the transaction will not make it into a block,
|
||||
/// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower
|
||||
/// level [`TxProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_finalized(mut self) -> Result<TxInBlock<T, C>, Error> {
|
||||
while let Some(status) = self.next_item().await {
|
||||
match status? {
|
||||
// Finalized! Return.
|
||||
TxStatus::Finalized(s) => return Ok(s),
|
||||
// Error scenarios; return the error.
|
||||
TxStatus::FinalityTimeout(_) => {
|
||||
return Err(TransactionError::FinalitySubscriptionTimeout.into())
|
||||
}
|
||||
// Ignore and wait for next status event:
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Err(RpcError::Custom("RPC subscription dropped".into()).into())
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be finalized, and for the transaction events to indicate
|
||||
/// that the transaction was successful. Returns the events associated with the transaction,
|
||||
/// as well as a couple of other details (block hash and extrinsic hash).
|
||||
///
|
||||
/// **Note:** consumes self. If you'd like to perform multiple actions as progress is made,
|
||||
/// use [`TxProgress::next_item()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they
|
||||
/// may well indicate with some probability that the transaction will not make it into a block,
|
||||
/// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower
|
||||
/// level [`TxProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_finalized_success(self) -> Result<TxEvents<T>, Error> {
|
||||
let evs = self.wait_for_finalized().await?.wait_for_success().await?;
|
||||
Ok(evs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OnlineClientT<T>> Stream for TxProgress<T, C> {
|
||||
type Item = Result<TxStatus<T, C>, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let sub = match self.sub.as_mut() {
|
||||
Some(sub) => sub,
|
||||
None => return Poll::Ready(None),
|
||||
};
|
||||
|
||||
sub.poll_next_unpin(cx)
|
||||
.map_err(|e| e.into())
|
||||
.map_ok(|status| {
|
||||
match status {
|
||||
SubstrateTxStatus::Future => TxStatus::Future,
|
||||
SubstrateTxStatus::Ready => TxStatus::Ready,
|
||||
SubstrateTxStatus::Broadcast(peers) => TxStatus::Broadcast(peers),
|
||||
SubstrateTxStatus::InBlock(hash) => {
|
||||
TxStatus::InBlock(TxInBlock::new(
|
||||
hash,
|
||||
self.ext_hash,
|
||||
self.client.clone(),
|
||||
))
|
||||
}
|
||||
SubstrateTxStatus::Retracted(hash) => TxStatus::Retracted(hash),
|
||||
SubstrateTxStatus::Usurped(hash) => TxStatus::Usurped(hash),
|
||||
SubstrateTxStatus::Dropped => TxStatus::Dropped,
|
||||
SubstrateTxStatus::Invalid => TxStatus::Invalid,
|
||||
// Only the following statuses are actually considered "final" (see the substrate
|
||||
// docs on `TxStatus`). Basically, either the transaction makes it into a
|
||||
// block, or we eventually give up on waiting for it to make it into a block.
|
||||
// Even `Dropped`/`Invalid`/`Usurped` transactions might make it into a block eventually.
|
||||
//
|
||||
// As an example, a transaction that is `Invalid` on one node due to having the wrong
|
||||
// nonce might still be valid on some fork on another node which ends up being finalized.
|
||||
// Equally, a transaction `Dropped` from one node may still be in the transaction pool,
|
||||
// and make it into a block, on another node. Likewise with `Usurped`.
|
||||
SubstrateTxStatus::FinalityTimeout(hash) => {
|
||||
self.sub = None;
|
||||
TxStatus::FinalityTimeout(hash)
|
||||
}
|
||||
SubstrateTxStatus::Finalized(hash) => {
|
||||
self.sub = None;
|
||||
TxStatus::Finalized(TxInBlock::new(
|
||||
hash,
|
||||
self.ext_hash,
|
||||
self.client.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//* 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.
|
||||
///
|
||||
/// Note that there are conditions that may cause transactions to reappear in the pool:
|
||||
///
|
||||
/// 1. Due to possible forks, the transaction that ends up being included
|
||||
/// in one block may later re-enter the pool or be marked as invalid.
|
||||
/// 2. A transaction that is `Dropped` at one point may later re-enter the pool if
|
||||
/// some other transactions are removed.
|
||||
/// 3. `Invalid` transactions may become valid at some point in the future.
|
||||
/// (Note that runtimes are encouraged to use `UnknownValidity` to inform the
|
||||
/// pool about such cases).
|
||||
/// 4. `Retracted` transactions might be included in a future block.
|
||||
///
|
||||
/// The stream is considered finished only when either the `Finalized` or `FinalityTimeout`
|
||||
/// event is triggered. You are however free to unsubscribe from notifications at any point.
|
||||
/// The first one will be emitted when the block in which the transaction was included gets
|
||||
/// finalized. The `FinalityTimeout` event will be emitted when the block did not reach finality
|
||||
/// within 512 blocks. This either indicates that finality is not available for your chain,
|
||||
/// or that finality gadget is lagging behind.
|
||||
#[derive(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,
|
||||
}
|
||||
|
||||
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`].
|
||||
pub fn as_finalized(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::Finalized(val) => Some(val),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A convenience method to return the `InBlock` details. Returns
|
||||
/// [`None`] if the enum variant is not [`TxStatus::InBlock`].
|
||||
pub fn as_in_block(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::InBlock(val) => Some(val),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct represents a transaction that has made it into a block.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
|
||||
pub struct TxInBlock<T: Config, C> {
|
||||
block_hash: T::Hash,
|
||||
ext_hash: T::Hash,
|
||||
client: C,
|
||||
}
|
||||
|
||||
impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
||||
pub(crate) fn new(block_hash: T::Hash, ext_hash: T::Hash, client: C) -> Self {
|
||||
Self {
|
||||
block_hash,
|
||||
ext_hash,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hash of the block that the transaction has made it into.
|
||||
pub fn block_hash(&self) -> T::Hash {
|
||||
self.block_hash
|
||||
}
|
||||
|
||||
/// Return the hash of the extrinsic that was submitted.
|
||||
pub fn extrinsic_hash(&self) -> T::Hash {
|
||||
self.ext_hash
|
||||
}
|
||||
|
||||
/// Fetch the events associated with this transaction. If the transaction
|
||||
/// was successful (ie no `ExtrinsicFailed`) events were found, then we return
|
||||
/// the events associated with it. If the transaction was not successful, or
|
||||
/// something else went wrong, we return an error.
|
||||
///
|
||||
/// **Note:** If multiple `ExtrinsicFailed` errors are returned (for instance
|
||||
/// because a pallet chooses to emit one as an event, which is considered
|
||||
/// abnormal behaviour), it is not specified which of the errors is returned here.
|
||||
/// You can use [`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<TxEvents<T>, Error> {
|
||||
let events = self.fetch_events().await?;
|
||||
|
||||
// Try to find any errors; return the first one we encounter.
|
||||
for ev in events.iter() {
|
||||
let ev = ev?;
|
||||
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
|
||||
let dispatch_error =
|
||||
DispatchError::decode_from(ev.field_bytes(), &self.client.metadata());
|
||||
return Err(dispatch_error.into())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// 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<TxEvents<T>, Error> {
|
||||
let block = self
|
||||
.client
|
||||
.rpc()
|
||||
.block(Some(self.block_hash))
|
||||
.await?
|
||||
.ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?;
|
||||
|
||||
let extrinsic_idx = block.block.extrinsics
|
||||
.iter()
|
||||
.position(|ext| {
|
||||
let hash = T::Hashing::hash_of(ext);
|
||||
hash == self.ext_hash
|
||||
})
|
||||
// If we successfully obtain the block hash we think contains our
|
||||
// extrinsic, the extrinsic should be in there somewhere..
|
||||
.ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?;
|
||||
|
||||
let events = EventsClient::new(self.client.clone())
|
||||
.at(Some(self.block_hash))
|
||||
.await?;
|
||||
|
||||
Ok(TxEvents {
|
||||
ext_hash: self.ext_hash,
|
||||
ext_idx: extrinsic_idx as u32,
|
||||
events,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents the events related to our transaction.
|
||||
/// We can iterate over the events, or look for a specific one.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""))]
|
||||
pub struct TxEvents<T: Config> {
|
||||
ext_hash: T::Hash,
|
||||
ext_idx: u32,
|
||||
events: Events<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> TxEvents<T> {
|
||||
/// Return the hash of the block that the transaction has made it into.
|
||||
pub fn block_hash(&self) -> T::Hash {
|
||||
self.events.block_hash()
|
||||
}
|
||||
|
||||
/// Return the hash of the extrinsic.
|
||||
pub fn extrinsic_hash(&self) -> T::Hash {
|
||||
self.ext_hash
|
||||
}
|
||||
|
||||
/// Return all of the events in the block that the transaction made it into.
|
||||
pub fn all_events_in_block(&self) -> &events::Events<T> {
|
||||
&self.events
|
||||
}
|
||||
|
||||
/// Iterate over all of the raw events associated with this transaction.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::iter()`] does, with the
|
||||
/// exception that it filters out events not related to the submitted extrinsic.
|
||||
pub fn iter(&self) -> impl Iterator<Item = Result<EventDetails, Error>> + '_ {
|
||||
self.events.iter().filter(|ev| {
|
||||
ev.as_ref()
|
||||
.map(|ev| ev.phase() == Phase::ApplyExtrinsic(self.ext_idx))
|
||||
.unwrap_or(true) // Keep any errors.
|
||||
})
|
||||
}
|
||||
|
||||
/// Find all of the transaction events matching the event type provided as a generic parameter.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::find()`] does, with the
|
||||
/// exception that it filters out events not related to the submitted extrinsic.
|
||||
pub fn find<Ev: StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, Error>> + '_ {
|
||||
self.iter().filter_map(|ev| {
|
||||
ev.and_then(|ev| ev.as_event::<Ev>().map_err(Into::into))
|
||||
.transpose()
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the transaction events using metadata to dynamically decode and skip
|
||||
/// them, and return the first event found which decodes to the provided `Ev` type.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::find_first()`] does, with the
|
||||
/// exception that it ignores events not related to the submitted extrinsic.
|
||||
pub fn find_first<Ev: StaticEvent>(&self) -> Result<Option<Ev>, Error> {
|
||||
self.find::<Ev>().next().transpose()
|
||||
}
|
||||
|
||||
/// Find an event in those associated with this transaction. Returns true if it was found.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::has()`] does, with the
|
||||
/// exception that it ignores events not related to the submitted extrinsic.
|
||||
pub fn has<Ev: StaticEvent>(&self) -> Result<bool, Error> {
|
||||
Ok(self.find::<Ev>().next().transpose()?.is_some())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user