mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 14:01:02 +00:00
811 lines
31 KiB
Rust
811 lines
31 KiB
Rust
//! This module exposes [`TransactionsClient`], which has methods for constructing and submitting
|
|
//! transactions. It's created by calling [`crate::client::ClientAtBlock::transactions()`], or
|
|
//! [`crate::client::ClientAtBlock::tx()`] for short.
|
|
//!
|
|
//! ```rust,no_run
|
|
//! pub use subxt::{OnlineClient, PolkadotConfig};
|
|
//!
|
|
//! let client = OnlineClient::new().await?;
|
|
//! let at_block = client.at_current_block().await?;
|
|
//!
|
|
//! let transactions = at_block.transactions();
|
|
//! ```
|
|
|
|
mod account_nonce;
|
|
mod default_params;
|
|
mod payload;
|
|
mod signer;
|
|
mod transaction_progress;
|
|
mod validation_result;
|
|
|
|
use crate::backend::{BackendExt, TransactionStatus as BackendTransactionStatus};
|
|
use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT};
|
|
use crate::config::extrinsic_params::Params;
|
|
use crate::config::{
|
|
ClientState, Config, ExtrinsicParams, ExtrinsicParamsEncoder, HashFor, Hasher, Header,
|
|
};
|
|
use crate::error::{ExtrinsicError, TransactionStatusError};
|
|
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::{DynamicPayload, Payload, StaticPayload, dynamic};
|
|
pub use signer::Signer;
|
|
pub use transaction_progress::{TransactionInBlock, TransactionProgress, TransactionStatus};
|
|
pub use validation_result::{
|
|
TransactionInvalid, TransactionUnknown, TransactionValid, ValidationResult,
|
|
};
|
|
|
|
/// A client for working with transactions. See [the module docs](crate::transactions) for more.
|
|
#[derive(Clone)]
|
|
pub struct TransactionsClient<'atblock, T, Client> {
|
|
client: &'atblock Client,
|
|
marker: PhantomData<T>,
|
|
}
|
|
|
|
impl<'atblock, T, Client> TransactionsClient<'atblock, T, Client> {
|
|
pub(crate) fn new(client: &'atblock Client) -> Self {
|
|
TransactionsClient {
|
|
client,
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'atblock, T: Config, Client: OfflineClientAtBlockT<T>>
|
|
TransactionsClient<'atblock, 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
|
|
/// the pallet or call in question do not exist at all).
|
|
pub fn validate<Call>(&self, call: &Call) -> Result<(), ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
let Some(details) = call.validation_details() else {
|
|
return Ok(());
|
|
};
|
|
|
|
let pallet_name = details.pallet_name;
|
|
let call_name = details.call_name;
|
|
|
|
let expected_hash = self
|
|
.client
|
|
.metadata_ref()
|
|
.pallet_by_name(pallet_name)
|
|
.ok_or_else(|| ExtrinsicError::PalletNameNotFound(pallet_name.to_string()))?
|
|
.call_hash(call_name)
|
|
.ok_or_else(|| ExtrinsicError::CallNameNotFound {
|
|
pallet_name: pallet_name.to_string(),
|
|
call_name: call_name.to_string(),
|
|
})?;
|
|
|
|
if details.hash != expected_hash {
|
|
Err(ExtrinsicError::IncompatibleCodegen)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Create a [`SubmittableTransaction`] from some already-signed and prepared
|
|
/// transaction bytes, and some client (anything implementing [`OfflineClientAtBlockT`]
|
|
/// or [`OnlineClientAtBlockT`]).
|
|
pub fn from_bytes(
|
|
client: &'atblock Client,
|
|
tx_bytes: Vec<u8>,
|
|
) -> SubmittableTransaction<'atblock, T, Client> {
|
|
SubmittableTransaction {
|
|
client,
|
|
encoded: tx_bytes,
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
|
|
/// Return the SCALE encoded bytes representing the call data of the transaction.
|
|
pub fn call_data<Call>(&self, call: &Call) -> Result<Vec<u8>, ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
let mut bytes = Vec::new();
|
|
let metadata = self.client.metadata_ref();
|
|
call.encode_call_data_to(metadata, &mut bytes)?;
|
|
Ok(bytes)
|
|
}
|
|
|
|
/// 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_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<'atblock, T, Client>, ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
let tx = match self.default_transaction_version()? {
|
|
SupportedTransactionVersion::V4 => self.create_v4_unsigned(call),
|
|
SupportedTransactionVersion::V5 => self.create_v5_unsigned(call),
|
|
}?;
|
|
|
|
Ok(tx)
|
|
}
|
|
|
|
/// Creates a V4 "unsigned" transaction without submitting it.
|
|
pub fn create_v4_unsigned<Call>(
|
|
&self,
|
|
call: &Call,
|
|
) -> Result<SubmittableTransaction<'atblock, T, Client>, ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
self.create_unsigned_at_version(call, SupportedTransactionVersion::V4)
|
|
}
|
|
|
|
/// Creates a V5 "bare" transaction without submitting it.
|
|
pub fn create_v5_unsigned<Call>(
|
|
&self,
|
|
call: &Call,
|
|
) -> Result<SubmittableTransaction<'atblock, T, Client>, ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
self.create_unsigned_at_version(call, SupportedTransactionVersion::V5)
|
|
}
|
|
|
|
/// 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_signable_offline<Call>(
|
|
&self,
|
|
call: &Call,
|
|
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
|
) -> Result<SignableTransaction<'atblock, T, Client>, ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
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.
|
|
///
|
|
/// 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_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_signable_offline<Call>(
|
|
&self,
|
|
call: &Call,
|
|
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
|
) -> Result<SignableTransaction<'atblock, T, Client>, ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
self.create_signable_at_version(call, params, SupportedTransactionVersion::V4)
|
|
}
|
|
|
|
/// Create a v5 partial transaction, ready to sign.
|
|
///
|
|
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_.
|
|
/// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values.
|
|
///
|
|
/// Prefer [`Self::create_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_signable_offline<Call>(
|
|
&self,
|
|
call: &Call,
|
|
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
|
) -> Result<SignableTransaction<'atblock, T, Client>, ExtrinsicError>
|
|
where
|
|
Call: Payload,
|
|
{
|
|
self.create_signable_at_version(call, params, SupportedTransactionVersion::V5)
|
|
}
|
|
|
|
/// 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: SupportedTransactionVersion,
|
|
) -> Result<SubmittableTransaction<'atblock, T, Client>, ExtrinsicError> {
|
|
let metadata = self.client.metadata_ref();
|
|
|
|
// 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. Encode extrinsic
|
|
let extrinsic = {
|
|
let mut encoded_inner = Vec::new();
|
|
// encode the transaction version first.
|
|
(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:
|
|
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.
|
|
Ok(SubmittableTransaction {
|
|
client: self.client,
|
|
encoded: extrinsic,
|
|
marker: PhantomData,
|
|
})
|
|
}
|
|
|
|
// Create a V4 "signed" or a V5 "general" transaction.
|
|
fn create_signable_at_version<Call>(
|
|
&self,
|
|
call: &Call,
|
|
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
|
tx_version: SupportedTransactionVersion,
|
|
) -> Result<SignableTransaction<'atblock, 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)?;
|
|
|
|
// 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,
|
|
call_data,
|
|
additional_and_extra_params,
|
|
tx_extension_version,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<'atblock, T: Config, Client: OnlineClientAtBlockT<T>> TransactionsClient<'atblock, 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<'atblock, 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<'atblock, 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<'atblock, 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<'atblock, 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<'atblock, 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<'atblock, 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)]
|
|
#[repr(u8)]
|
|
pub enum SupportedTransactionVersion {
|
|
/// v4 transactions (signed and unsigned transactions)
|
|
V4 = 4u8,
|
|
/// v5 transactions (bare and general transactions)
|
|
V5 = 5u8,
|
|
}
|
|
|
|
/// This is a transaction that requires signing before it can be submitted.
|
|
pub struct SignableTransaction<'atblock, T: Config, Client> {
|
|
client: &'atblock 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<'atblock, T: Config, Client: OfflineClientAtBlockT<T>>
|
|
SignableTransaction<'atblock, 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<'atblock, 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 [`SignableTransaction`] 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
|
|
/// [`SignableTransaction::sign()`] instead.
|
|
pub fn sign_with_account_and_signature(
|
|
&mut self,
|
|
account_id: &T::AccountId,
|
|
signature: &T::Signature,
|
|
) -> SubmittableTransaction<'atblock, 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.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,
|
|
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<'atblock, T, Client> {
|
|
client: &'atblock Client,
|
|
encoded: Vec<u8>,
|
|
marker: PhantomData<T>,
|
|
}
|
|
|
|
impl<'atblock, T, Client> SubmittableTransaction<'atblock, T, Client>
|
|
where
|
|
T: Config,
|
|
Client: OfflineClientAtBlockT<T>,
|
|
{
|
|
/// 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<'atblock, T: Config, Client: OnlineClientAtBlockT<T>>
|
|
SubmittableTransaction<'atblock, 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<'atblock, 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, 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 {
|
|
BackendTransactionStatus::Validated
|
|
| BackendTransactionStatus::Broadcasted
|
|
| BackendTransactionStatus::InBestBlock { .. }
|
|
| BackendTransactionStatus::NoLongerInBestBlock
|
|
| BackendTransactionStatus::InFinalizedBlock { .. } => Ok(ext_hash),
|
|
BackendTransactionStatus::Error { message } => Err(
|
|
ExtrinsicError::TransactionStatusError(TransactionStatusError::Error(message)),
|
|
),
|
|
BackendTransactionStatus::Invalid { message } => {
|
|
Err(ExtrinsicError::TransactionStatusError(
|
|
TransactionStatusError::Invalid(message),
|
|
))
|
|
}
|
|
BackendTransactionStatus::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(¶ms),
|
|
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(¶ms),
|
|
latest_block_ref.hash(),
|
|
)
|
|
.await
|
|
.map_err(ExtrinsicError::CannotGetFeeInfo)?;
|
|
|
|
Ok(partial_fee)
|
|
}
|
|
}
|