Impl most transaction APIs. TxProgress and Events next

This commit is contained in:
James Wilson
2025-12-03 16:44:05 +00:00
parent c1ebceb965
commit b85a412ecb
10 changed files with 877 additions and 110 deletions
+6 -6
View File
@@ -11,12 +11,12 @@ pub use online_client::{OnlineClient, OnlineClientAtBlock, OnlineClientAtBlockT}
/// This represents a client at a specific block number.
#[derive(Clone, Debug)]
pub struct ClientAtBlock<Client, T> {
pub struct ClientAtBlock<T, Client> {
client: Client,
marker: PhantomData<T>,
}
impl<Client, T> ClientAtBlock<Client, T> {
impl<T, Client> ClientAtBlock<T, Client> {
/// Construct a new client at some block.
pub(crate) fn new(client: Client) -> Self {
Self {
@@ -26,14 +26,14 @@ impl<Client, T> ClientAtBlock<Client, T> {
}
}
impl<Client, T> ClientAtBlock<Client, T>
impl<T, Client> ClientAtBlock<T, Client>
where
T: Config,
Client: OfflineClientAtBlockT<T>,
{
/// Construct transactions.
pub fn tx(&self) -> Transactions<'_, T, Client> {
Transactions::new(&self.client)
pub fn tx(&self) -> Transactions<T, Client> {
Transactions::new(self.client.clone())
}
/// Obtain a reference to the metadata.
@@ -47,7 +47,7 @@ where
}
}
impl<Client, T> ClientAtBlock<Client, T>
impl<T, Client> ClientAtBlock<T, Client>
where
T: Config,
Client: OnlineClientAtBlockT<T>,
+11 -2
View File
@@ -1,5 +1,5 @@
use crate::client::ClientAtBlock;
use crate::config::{Config, HashFor};
use crate::config::{Config, HashFor, Hasher};
use crate::error::OfflineClientAtBlockError;
use std::sync::Arc;
use subxt_metadata::Metadata;
@@ -21,7 +21,7 @@ impl<T: Config> OfflineClient<T> {
pub fn at_block(
&self,
block_number: impl Into<u64>,
) -> Result<ClientAtBlock<OfflineClientAtBlock<T>, T>, OfflineClientAtBlockError> {
) -> Result<ClientAtBlock<T, OfflineClientAtBlock<T>>, OfflineClientAtBlockError> {
let block_number = block_number.into();
let (spec_version, transaction_version) = self
.config
@@ -35,11 +35,14 @@ impl<T: Config> OfflineClient<T> {
let genesis_hash = self.config.genesis_hash();
let hasher = <T::Hasher as Hasher>::new(&metadata);
let offline_client_at_block = OfflineClientAtBlock {
metadata,
block_number,
genesis_hash,
spec_version,
hasher,
transaction_version,
};
@@ -53,6 +56,7 @@ pub struct OfflineClientAtBlock<T: Config> {
block_number: u64,
genesis_hash: Option<HashFor<T>>,
spec_version: u32,
hasher: T::Hasher,
transaction_version: u32,
}
@@ -69,6 +73,8 @@ pub trait OfflineClientAtBlockT<T: Config>: Clone {
fn genesis_hash(&self) -> Option<HashFor<T>>;
/// The spec version at the current block.
fn spec_version(&self) -> u32;
/// Return a hasher that works at the current block.
fn hasher(&self) -> &T::Hasher;
/// The transaction version at the current block.
///
/// Note: This is _not_ the same as the transaction version that
@@ -95,4 +101,7 @@ impl<T: Config> OfflineClientAtBlockT<T> for OfflineClientAtBlock<T> {
fn transaction_version(&self) -> u32 {
self.transaction_version
}
fn hasher(&self) -> &T::Hasher {
&self.hasher
}
}
+6 -8
View File
@@ -210,7 +210,7 @@ impl<T: Config> OnlineClient<T> {
/// This does not track new blocks.
pub async fn at_current_block(
&self,
) -> Result<ClientAtBlock<OnlineClientAtBlock<T>, T>, OnlineClientAtBlockError> {
) -> Result<ClientAtBlock<T, OnlineClientAtBlock<T>>, OnlineClientAtBlockError> {
let latest_block = self
.inner
.backend
@@ -225,7 +225,7 @@ impl<T: Config> OnlineClient<T> {
pub async fn at_block(
&self,
number_or_hash: impl Into<BlockNumberOrRef<T>>,
) -> Result<ClientAtBlock<OnlineClientAtBlock<T>, T>, OnlineClientAtBlockError> {
) -> Result<ClientAtBlock<T, OnlineClientAtBlock<T>>, OnlineClientAtBlockError> {
let number_or_hash = number_or_hash.into();
// We are given either a block hash or number. We need both.
@@ -274,7 +274,7 @@ impl<T: Config> OnlineClient<T> {
&self,
block_ref: impl Into<BlockRef<HashFor<T>>>,
block_number: u64,
) -> Result<ClientAtBlock<OnlineClientAtBlock<T>, T>, OnlineClientAtBlockError> {
) -> Result<ClientAtBlock<T, OnlineClientAtBlock<T>>, OnlineClientAtBlockError> {
let block_ref = block_ref.into();
let block_hash = block_ref.hash();
@@ -452,8 +452,6 @@ pub trait OnlineClientAtBlockT<T: Config>: OfflineClientAtBlockT<T> {
fn backend(&self) -> &dyn Backend<T>;
/// Return the block hash for the current block.
fn block_hash(&self) -> HashFor<T>;
/// Return a hasher that works at the current block.
fn hasher(&self) -> &T::Hasher;
}
/// The inner type providing the necessary data to work online at a specific block.
@@ -476,9 +474,6 @@ impl<T: Config> OnlineClientAtBlockT<T> for OnlineClientAtBlock<T> {
fn block_hash(&self) -> HashFor<T> {
self.block_ref.hash()
}
fn hasher(&self) -> &T::Hasher {
&self.hasher
}
}
impl<T: Config> OfflineClientAtBlockT<T> for OnlineClientAtBlock<T> {
@@ -500,6 +495,9 @@ impl<T: Config> OfflineClientAtBlockT<T> for OnlineClientAtBlock<T> {
fn transaction_version(&self) -> u32 {
self.transaction_version
}
fn hasher(&self) -> &T::Hasher {
&self.hasher
}
}
fn get_legacy_types<'a, T: Config, Md: ToTypeRegistry>(
+1 -1
View File
@@ -70,7 +70,7 @@ impl<T: Config> Block<T> {
/// Instantiate a client at this block.
pub async fn client(
&self,
) -> Result<ClientAtBlock<OnlineClientAtBlock<T>, T>, OnlineClientAtBlockError> {
) -> Result<ClientAtBlock<T, OnlineClientAtBlock<T>>, OnlineClientAtBlockError> {
self.client.at_block(self.block_ref.clone()).await
}
}
+2 -2
View File
@@ -9,8 +9,8 @@
//! Polkadot node.
mod default_extrinsic_params;
mod extrinsic_params;
pub mod extrinsic_params;
pub mod polkadot;
pub mod substrate;
pub mod transaction_extensions;
@@ -40,7 +40,7 @@ pub trait Config: Clone + Debug + Sized + Send + Sync + 'static {
type AccountId: Debug + Clone + Encode + Decode + Serialize + Send;
/// The address type; required for constructing extrinsics.
type Address: Debug + Encode + From<<Self as Config>::AccountId>;
type Address: Debug + Encode + From<Self::AccountId>;
/// The signature type.
type Signature: Debug + Clone + Encode + Decode + Send;
+2
View File
@@ -582,6 +582,8 @@ pub enum ExtrinsicError {
},
#[error("Can't encode the extrinsic call data: {0}")]
CannotEncodeCallData(scale_encode::Error),
#[error("Failed to encode an extrinsic: the genesis hash was not provided")]
GenesisHashNotProvided,
#[error("Subxt does not support the extrinsic versions expected by the chain")]
UnsupportedVersion,
#[error("Cannot construct the required transaction extensions: {0}")]
+612 -91
View File
@@ -1,24 +1,38 @@
mod account_nonce;
mod default_params;
mod payload;
mod signer;
mod validation_result;
use crate::backend::BackendExt;
use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT};
use crate::config::{ClientState, Config};
use crate::config::extrinsic_params::Params;
use crate::config::{
ClientState, Config, ExtrinsicParams, ExtrinsicParamsEncoder, HashFor, Hasher, Header,
};
use crate::error::ExtrinsicError;
use codec::Compact;
use codec::{Compact, Encode};
use core::marker::PhantomData;
use futures::{TryFutureExt, future::try_join};
use sp_crypto_hashing::blake2_256;
use std::borrow::Cow;
pub use default_params::DefaultParams;
pub use payload::Payload;
pub use signer::Signer;
pub use validation_result::{
TransactionInvalid, TransactionUnknown, TransactionValid, ValidationResult,
};
/// A client for working with transactions.
#[derive(Clone)]
pub struct Transactions<'atblock, T, Client> {
client: &'atblock Client,
pub struct Transactions<T, Client> {
client: Client,
marker: PhantomData<T>,
}
impl<'atblock, T, Client> Transactions<'atblock, T, Client> {
pub(crate) fn new(client: &'atblock Client) -> Self {
impl<T, Client> Transactions<T, Client> {
pub(crate) fn new(client: Client) -> Self {
Transactions {
client,
marker: PhantomData,
@@ -26,7 +40,7 @@ impl<'atblock, T, Client> Transactions<'atblock, T, Client> {
}
}
impl<'atblock, T: Config, C: OfflineClientAtBlockT<T>> Transactions<'atblock, T, C> {
impl<T: Config, Client: OfflineClientAtBlockT<T>> Transactions<T, Client> {
/// 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
@@ -73,75 +87,62 @@ impl<'atblock, T: Config, C: OfflineClientAtBlockT<T>> Transactions<'atblock, T,
/// 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.
/// [`Self::create_v5_unsigned`] 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>
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
let tx = match self.default_transaction_version()? {
TransactionVersion::V4 => self.create_v4_unsigned(call),
TransactionVersion::V5 => self.create_v5_bare(call),
SupportedTransactionVersion::V4 => self.create_v4_unsigned(call),
SupportedTransactionVersion::V5 => self.create_v5_unsigned(call),
}?;
Ok(SubmittableTransaction {
client: self.client.clone(),
inner: tx,
})
Ok(tx)
}
/// Creates a V4 "unsigned" transaction without submitting it.
pub fn create_v4_unsigned<Call>(&self, call: &Call) -> Result<Transaction<T>, ExtrinsicError>
pub fn create_v4_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
self.create_unsigned_at_version(call, 4)
self.create_unsigned_at_version(call, SupportedTransactionVersion::V4)
}
/// Creates a V5 "bare" transaction without submitting it.
pub fn create_v5_bare<Call>(&self, call: &Call) -> Result<Transaction<T>, ExtrinsicError>
pub fn create_v5_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
self.create_unsigned_at_version(call, 5)
self.create_unsigned_at_version(call, SupportedTransactionVersion::V5)
}
/// Create a partial transaction. Depending on the metadata, we might end up constructing either a v4 or
/// v5 transaction. See [`subxt_core::tx`] if you'd like to manually pick the version to construct
/// Create a signable transaction. Depending on the metadata, we might end up constructing either a v4 or
/// v5 transaction. Use [`Self::create_v4_signable_offline`] or [`Self::create_v5_signable_offline`] if you'd
/// like to manually use a specific version.
///
/// 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>(
pub fn create_signable_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
) -> Result<SignableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let client_state = ClientState {
genesis_hash: self.client.genesis_hash(),
spec_version: self.client.spec_version(),
transaction_version: self.client.transaction_version(),
metadata: self.client.metadata(),
};
let tx = match self.default_transaction_version()? {
TransactionVersion::V4 => {
PartialTransactionInner::V4(self.create_v4_signed(call, &client_state, params)?)
}
TransactionVersion::V5 => {
PartialTransactionInner::V5(self.create_v5_general(call, &client_state, params)?)
}
};
Ok(PartialTransaction {
client: self.client.clone(),
inner: tx,
})
match self.default_transaction_version()? {
SupportedTransactionVersion::V4 => self.create_v4_signable_offline(call, params),
SupportedTransactionVersion::V5 => self.create_v5_signable_offline(call, params),
}
}
/// Create a v4 partial transaction, ready to sign.
@@ -149,26 +150,17 @@ impl<'atblock, T: Config, C: OfflineClientAtBlockT<T>> Transactions<'atblock, T,
/// 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
/// Prefer [`Self::create_signable_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>(
pub fn create_v4_signable_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
) -> Result<SignableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
let tx = PartialTransactionInner::V4(subxt_core::tx::create_v4_signed(
call,
&self.client.client_state(),
params,
)?);
Ok(PartialTransaction {
client: self.client.clone(),
inner: tx,
})
self.create_signable_at_version(call, params, SupportedTransactionVersion::V4)
}
/// Create a v5 partial transaction, ready to sign.
@@ -176,34 +168,45 @@ impl<'atblock, T: Config, C: OfflineClientAtBlockT<T>> Transactions<'atblock, T,
/// 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
/// Prefer [`Self::create_signable_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>(
pub fn create_v5_signable_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
) -> Result<SignableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
let tx = PartialTransactionInner::V5(subxt_core::tx::create_v5_general(
call,
&self.client.client_state(),
params,
)?);
self.create_signable_at_version(call, params, SupportedTransactionVersion::V5)
}
Ok(PartialTransaction {
client: self.client.clone(),
inner: tx,
})
/// Returns the suggested transaction versions to build for a given chain, or an error
/// if Subxt doesn't support any version expected by the chain.
///
/// When using methods like [`Self::create_signable_offline`] and [`Self::create_unsigned`],
/// this will be used internally to decide which transaction version to construct.
pub fn default_transaction_version(
&self,
) -> Result<SupportedTransactionVersion, ExtrinsicError> {
let metadata = self.client.metadata_ref();
let versions = metadata.extrinsic().supported_versions();
if versions.contains(&4) {
Ok(SupportedTransactionVersion::V4)
} else if versions.contains(&5) {
Ok(SupportedTransactionVersion::V5)
} else {
Err(ExtrinsicError::UnsupportedVersion)
}
}
// Create a V4 "unsigned" transaction or V5 "bare" transaction.
fn create_unsigned_at_version<Call: Payload>(
&self,
call: &Call,
tx_version: u8,
) -> Result<Transaction<T>, ExtrinsicError> {
tx_version: SupportedTransactionVersion,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError> {
let metadata = self.client.metadata_ref();
// 1. Validate this call against the current node metadata if the call comes
@@ -214,7 +217,7 @@ impl<'atblock, T: Config, C: OfflineClientAtBlockT<T>> Transactions<'atblock, T,
let extrinsic = {
let mut encoded_inner = Vec::new();
// encode the transaction version first.
tx_version.encode_to(&mut encoded_inner);
(tx_version as u8).encode_to(&mut encoded_inner);
// encode call data after this byte.
call.encode_call_data_to(metadata, &mut encoded_inner)?;
// now, prefix byte length:
@@ -228,33 +231,551 @@ impl<'atblock, T: Config, C: OfflineClientAtBlockT<T>> Transactions<'atblock, T,
};
// Wrap in Encoded to ensure that any more "encode" calls leave it in the right state.
Ok(Transaction::from_bytes(extrinsic))
Ok(SubmittableTransaction {
client: self.client.clone(),
encoded: extrinsic,
marker: PhantomData,
})
}
/// Returns the suggested transaction versions to build for a given chain, or an error
/// if Subxt doesn't support any version expected by the chain.
///
/// If the result is [`TransactionVersion::V4`], use the `v4` methods in this module. If it's
/// [`TransactionVersion::V5`], use the `v5` ones.
pub fn default_transaction_version(&self) -> Result<TransactionVersion, ExtrinsicError> {
let metadata = self.client.metadata_ref();
let versions = metadata.extrinsic().supported_versions();
// Create a V4 "signed" or a V5 "general" transaction.
pub fn create_signable_at_version<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
tx_version: SupportedTransactionVersion,
) -> Result<SignableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
// 1. Validate this call against the current node metadata if the call comes
// with a hash allowing us to do so.
self.validate(call)?;
if versions.contains(&4) {
Ok(TransactionVersion::V4)
} else if versions.contains(&5) {
Ok(TransactionVersion::V5)
} else {
Err(ExtrinsicError::UnsupportedVersion)
}
// 2. Work out which TX extension version to target based on metadata.
let tx_extension_version = match tx_version {
SupportedTransactionVersion::V4 => None,
SupportedTransactionVersion::V5 => {
let v = self
.client
.metadata_ref()
.extrinsic()
.transaction_extension_version_to_use_for_encoding();
Some(v)
}
};
// 3. SCALE encode call data to bytes (pallet u8, call u8, call params).
let call_data = self.call_data(call)?;
// 4. Construct our custom additional/extra params.
let client_state = ClientState {
genesis_hash: self
.client
.genesis_hash()
.ok_or(ExtrinsicError::GenesisHashNotProvided)?,
spec_version: self.client.spec_version(),
transaction_version: self.client.transaction_version(),
metadata: self.client.metadata(),
};
let additional_and_extra_params =
<T::ExtrinsicParams as ExtrinsicParams<T>>::new(&client_state, params)?;
// Return these details, ready to construct a signed extrinsic from.
Ok(SignableTransaction {
client: self.client.clone(),
call_data,
additional_and_extra_params,
tx_extension_version,
})
}
}
impl<T: Config, Client: OnlineClientAtBlockT<T>> Transactions<T, Client> {
/// Get the account nonce for a given account ID.
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, ExtrinsicError> {
account_nonce::get_account_nonce(&self.client, account_id)
.await
.map_err(|e| ExtrinsicError::AccountNonceError {
block_hash: self.client.block_hash().into(),
account_id: account_id.clone().encode().into(),
reason: e,
})
}
/// Creates a signable transaction. This can then be signed and submitted.
pub async fn create_signable<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<SignableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
self.inject_account_nonce_and_block(account_id, &mut params)
.await?;
self.create_signable_offline(call, params)
}
/// Creates a signable V4 transaction, without submitting it. This can then be signed and submitted.
///
/// Prefer [`Self::create_signable()`] 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_signable<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<SignableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
self.inject_account_nonce_and_block(account_id, &mut params)
.await?;
self.create_v4_signable_offline(call, params)
}
/// Creates a signable V5 transaction, without submitting it. This can then be signed and submitted.
///
/// Prefer [`Self::create_signable()`] 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_signable<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<SignableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
self.inject_account_nonce_and_block(account_id, &mut params)
.await?;
self.create_v5_signable_offline(call, params)
}
/// Creates a signed transaction, without submitting it.
pub async fn create_signed<Call, S>(
&mut self,
call: &Call,
signer: &S,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
S: Signer<T>,
{
let mut signable = self
.create_signable(call, &signer.account_id(), params)
.await?;
Ok(signable.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 [`TransactionProgress`], which can be used to track the status of the transaction
/// and obtain details about it, once it has made it into a block.
pub async fn sign_and_submit_then_watch_default<Call, S>(
&mut self,
call: &Call,
signer: &S,
) -> Result<TransactionProgress<T, Client>, ExtrinsicError>
where
Call: Payload,
S: Signer<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 [`TransactionProgress`], which can be used to track the status of the transaction
/// and obtain details about it, once it has made it into a block.
pub async fn sign_and_submit_then_watch<Call, S>(
&mut self,
call: &Call,
signer: &S,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<TransactionProgress<T, Client>, ExtrinsicError>
where
Call: Payload,
S: Signer<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, S>(
&mut self,
call: &Call,
signer: &S,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
S: Signer<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, S>(
&mut self,
call: &Call,
signer: &S,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
S: Signer<T>,
{
self.create_signed(call, signer, params)
.await?
.submit()
.await
}
/// 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(
&self,
account_id: &T::AccountId,
params: &mut <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<(), ExtrinsicError> {
let block_ref = self
.client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
let (block_header, account_nonce) = try_join(
self.client
.backend()
.block_header(block_ref.hash())
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock),
self.account_nonce(account_id),
)
.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(), block_ref.hash());
Ok(())
}
}
/// The transaction versions supported by Subxt.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum TransactionVersion {
#[repr(u8)]
pub enum SupportedTransactionVersion {
/// v4 transactions (signed and unsigned transactions)
V4,
V4 = 4u8,
/// v5 transactions (bare and general transactions)
V5,
V5 = 5u8,
}
/// This is a transaction that requires signing before it can be submitted.
pub struct SignableTransaction<T: Config, Client> {
client: Client,
call_data: Vec<u8>,
additional_and_extra_params: <T as Config>::ExtrinsicParams,
// For V4 transactions this doesn't exist, and for V5 it does.
tx_extension_version: Option<u8>,
}
impl<T: Config, Client: OfflineClientAtBlockT<T>> SignableTransaction<T, Client> {
/// Return the bytes representing the call data for this partially constructed
/// transaction.
pub fn call_data(&self) -> &[u8] {
&self.call_data
}
/// 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> {
self.with_signer_payload(|bytes| bytes.to_vec())
}
/// Convert this [`SignableTransaction`] 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<S: Signer<T>>(&mut self, signer: &S) -> SubmittableTransaction<T, Client> {
// Given our signer, we can sign the payload representing this extrinsic.
let signature = signer.sign(&self.signer_payload());
// Now, use the signature and "from" account to build the extrinsic.
self.sign_with_account_and_signature(&signer.account_id(), &signature)
}
/// 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, Client> {
let encoded = if let Some(tx_extensions_version) = self.tx_extension_version {
let mut encoded_inner = Vec::new();
// Pass account and signature to extensions to be added.
self.additional_and_extra_params
.inject_signature(account_id, signature);
// "is general" + transaction protocol version (5)
(0b01000000 + 5u8).encode_to(&mut encoded_inner);
// Encode versions for the transaction extensions
(tx_extensions_version as u8).encode_to(&mut encoded_inner);
// Encode the actual transaction extensions values
self.additional_and_extra_params
.encode_value_to(&mut encoded_inner);
// and now, call data (remembering that it's been encoded already and just needs appending)
encoded_inner.extend(&self.call_data);
// 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
} else {
let mut encoded_inner = Vec::new();
// "is signed" + transaction protocol version (4)
(0b10000000 + 4u8).encode_to(&mut encoded_inner);
// from address for signature
let address: T::Address = account_id.clone().into();
address.encode_to(&mut encoded_inner);
// the signature
signature.encode_to(&mut encoded_inner);
// attach custom extra params
self.additional_and_extra_params
.encode_value_to(&mut encoded_inner);
// and now, call data (remembering that it's been encoded already and just needs appending)
encoded_inner.extend(&self.call_data);
// 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
};
SubmittableTransaction {
client: self.client.clone(),
encoded,
marker: PhantomData,
}
}
// Obtain bytes representing the signer payload and run call some function
// with them. This can avoid an allocation in some cases.
fn with_signer_payload<F, R>(&self, f: F) -> R
where
F: for<'a> FnOnce(Cow<'a, [u8]>) -> R,
{
let mut bytes = self.call_data.clone();
self.additional_and_extra_params
.encode_signer_payload_value_to(&mut bytes);
self.additional_and_extra_params
.encode_implicit_to(&mut bytes);
// For V5 transactions we _always_ blake2 hash. For V4 we only
// hash if more than 256 bytes in the payload.
if self.is_v5() || bytes.len() > 256 {
f(Cow::Borrowed(&blake2_256(&bytes)))
} else {
f(Cow::Owned(bytes))
}
}
// Are we working with a V5 transaction? This is handled a bit differently.
fn is_v5(&self) -> bool {
self.tx_extension_version.is_some()
}
}
/// This is a transaction that is ready to submit.
pub struct SubmittableTransaction<T, Client> {
client: Client,
encoded: Vec<u8>,
marker: PhantomData<T>,
}
impl<T, Client> SubmittableTransaction<T, Client>
where
T: Config,
Client: OfflineClientAtBlockT<T>,
{
/// Create a [`SubmittableTransaction`] from some already-signed and prepared
/// transaction bytes, and some client (anything implementing [`OfflineClientAtBlockT`]
/// or [`OnlineClientAtBlockT`]).
pub fn from_bytes(client: Client, tx_bytes: Vec<u8>) -> Self {
Self {
client,
encoded: tx_bytes,
marker: PhantomData,
}
}
/// Calculate and return the hash of the transaction, based on the configured hasher.
pub fn hash(&self) -> HashFor<T> {
self.client.hasher().hash(&self.encoded)
}
/// Returns the SCALE encoded transaction bytes.
pub fn encoded(&self) -> &[u8] {
&self.encoded
}
/// Consumes [`SubmittableTransaction`] and returns the SCALE encoded
/// transaction bytes.
pub fn into_encoded(self) -> Vec<u8> {
self.encoded.clone()
}
}
impl<T: Config, Client: OnlineClientAtBlockT<T>> SubmittableTransaction<T, Client> {
/// Submits the transaction to the chain.
///
/// Returns a [`TransactionProgress`], which can be used to track the status of the transaction
/// and obtain details about it, once it has made it into a block.
pub async fn submit_and_watch(&self) -> Result<TransactionProgress<T, Client>, 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(TransactionProgress::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 block_hash = self.client.block_hash();
// Approach taken from https://github.com/paritytech/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://paritytech.github.io/substrate/master/pallet_transaction_payment_rpc_runtime_api/struct.RuntimeDispatchInfo.html>
// data layout: {weight_ref_time: Compact<u64>, weight_proof_size: Compact<u64>, class: u8, partial_fee: u128}
let (_, _, _, partial_fee) = self
.client
.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)
}
}
+38
View File
@@ -0,0 +1,38 @@
use crate::client::OnlineClientAtBlockT;
use crate::config::Config;
use crate::error::AccountNonceError;
use codec::{Decode, Encode};
/// Return the account nonce at some block hash for an account ID.
pub async fn get_account_nonce<T, C>(
client: &C,
account_id: &T::AccountId,
) -> Result<u64, AccountNonceError>
where
T: Config,
C: OnlineClientAtBlockT<T>,
{
let block_hash = client.block_hash();
let account_nonce_bytes = client
.backend()
.call(
"AccountNonceApi_account_nonce",
Some(&account_id.encode()),
block_hash,
)
.await?;
// custom decoding from a u16/u32/u64 into a u64, based on the number of bytes we got back.
let cursor = &mut &account_nonce_bytes[..];
let account_nonce: u64 = match account_nonce_bytes.len() {
2 => u16::decode(cursor)?.into(),
4 => u32::decode(cursor)?.into(),
8 => u64::decode(cursor)?,
_ => {
return Err(AccountNonceError::WrongNumberOfBytes(
account_nonce_bytes.len(),
));
}
};
Ok(account_nonce)
}
+60
View File
@@ -0,0 +1,60 @@
/// 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);
};
+139
View File
@@ -0,0 +1,139 @@
use crate::error::ExtrinsicError;
use codec::Decode;
/// 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(_))
}
#[allow(clippy::get_first)]
pub(crate) fn try_from_bytes(bytes: Vec<u8>) -> Result<ValidationResult, ExtrinsicError> {
// TaggedTransactionQueue_validate_transaction returns this:
// https://github.com/paritytech/substrate/blob/0cdf7029017b70b7c83c21a4dc0aa1020e7914f6/primitives/runtime/src/transaction_validity.rs#L210
// We copy some of the inner types and put the three states (valid, invalid, unknown) into one enum,
// because from our perspective, the call was successful regardless.
if bytes.get(0) == Some(&0) {
// ok: valid. Decode but, for now we discard most of the information
let res = TransactionValid::decode(&mut &bytes[1..])
.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))
}
}
}
/// Transaction is valid; here is some more information about it.
#[derive(Decode, Clone, Debug, PartialEq)]
pub struct TransactionValid {
/// Priority of the transaction.
///
/// Priority determines the ordering of two transactions that have all
/// their dependencies (required tags) satisfied.
pub priority: u64,
/// Transaction dependencies
///
/// A non-empty list signifies that some other transactions which provide
/// given tags are required to be included before that one.
pub requires: Vec<Vec<u8>>,
/// Provided tags
///
/// A list of tags this transaction provides. Successfully importing the transaction
/// will enable other transactions that depend on (require) those tags to be included as well.
/// Provided and required tags allow Substrate to build a dependency graph of transactions
/// and import them in the right (linear) order.
pub provides: Vec<Vec<u8>>,
/// Transaction longevity
///
/// Longevity describes minimum number of blocks the validity is correct.
/// After this period transaction should be removed from the pool or revalidated.
pub longevity: u64,
/// A flag indicating if the transaction should be propagated to other peers.
///
/// By setting `false` here the transaction will still be considered for
/// including in blocks that are authored on the current node, but will
/// never be sent to other peers.
pub propagate: bool,
}
/// The runtime was unable to validate the transaction.
#[derive(Decode, Clone, Debug, PartialEq)]
pub enum TransactionUnknown {
/// Could not lookup some information that is required to validate the transaction.
CannotLookup,
/// No validator found for the given unsigned transaction.
NoUnsignedValidator,
/// Any other custom unknown validity that is not covered by this enum.
Custom(u8),
}
/// The transaction is invalid.
#[derive(Decode, Clone, Debug, PartialEq)]
pub enum TransactionInvalid {
/// The call of the transaction is not expected.
Call,
/// General error to do with the inability to pay some fees (e.g. account balance too low).
Payment,
/// General error to do with the transaction not yet being valid (e.g. nonce too high).
Future,
/// General error to do with the transaction being outdated (e.g. nonce too low).
Stale,
/// General error to do with the transaction's proofs (e.g. signature).
///
/// # Possible causes
///
/// When using a signed extension that provides additional data for signing, it is required
/// that the signing and the verifying side use the same additional data. Additional
/// data will only be used to generate the signature, but will not be part of the transaction
/// itself. As the verifying side does not know which additional data was used while signing
/// it will only be able to assume a bad signature and cannot express a more meaningful error.
BadProof,
/// The transaction birth block is ancient.
///
/// # Possible causes
///
/// For `FRAME`-based runtimes this would be caused by `current block number`
/// - Era::birth block number > BlockHashCount`. (e.g. in Polkadot `BlockHashCount` = 2400, so
/// a transaction with birth block number 1337 would be valid up until block number 1337 + 2400,
/// after which point the transaction would be considered to have an ancient birth block.)
AncientBirthBlock,
/// The transaction would exhaust the resources of current block.
///
/// The transaction might be valid, but there are not enough resources
/// left in the current block.
ExhaustsResources,
/// Any other custom invalid validity that is not covered by this enum.
Custom(u8),
/// An 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,
}