mod account_nonce; mod default_params; mod signer; mod transaction_progress; mod validation_result; pub mod payload; 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::Payload; pub use signer::Signer; pub use transaction_progress::{TransactionProgress, TransactionStatus}; pub use validation_result::{ TransactionInvalid, TransactionUnknown, TransactionValid, ValidationResult, }; /// A client for working with transactions. #[derive(Clone)] pub struct TransactionsClient<'atblock, T, Client> { client: &'atblock Client, marker: PhantomData, } 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> 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(&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, ) -> 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(&self, call: &Call) -> Result, 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( &self, call: &Call, ) -> Result, 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( &self, call: &Call, ) -> Result, 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( &self, call: &Call, ) -> Result, 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( &self, call: &Call, params: >::Params, ) -> Result, 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( &self, call: &Call, params: >::Params, ) -> Result, 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( &self, call: &Call, params: >::Params, ) -> Result, 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 { 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( &self, call: &Call, tx_version: SupportedTransactionVersion, ) -> Result, 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. pub fn create_signable_at_version( &self, call: &Call, params: >::Params, tx_version: SupportedTransactionVersion, ) -> Result, 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 = >::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> TransactionsClient<'atblock, T, Client> { /// Get the account nonce for a given account ID. pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result { 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( &self, call: &Call, account_id: &T::AccountId, mut params: >::Params, ) -> Result, 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( &self, call: &Call, account_id: &T::AccountId, mut params: >::Params, ) -> Result, 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( &self, call: &Call, account_id: &T::AccountId, mut params: >::Params, ) -> Result, 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( &mut self, call: &Call, signer: &S, params: >::Params, ) -> Result, ExtrinsicError> where Call: Payload, S: Signer, { 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( &mut self, call: &Call, signer: &S, ) -> Result, ExtrinsicError> where Call: Payload, S: Signer, >::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( &mut self, call: &Call, signer: &S, params: >::Params, ) -> Result, ExtrinsicError> where Call: Payload, S: Signer, { 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( &mut self, call: &Call, signer: &S, ) -> Result, ExtrinsicError> where Call: Payload, S: Signer, >::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( &mut self, call: &Call, signer: &S, params: >::Params, ) -> Result, ExtrinsicError> where Call: Payload, S: Signer, { 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 >::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, additional_and_extra_params: ::ExtrinsicParams, // For V4 transactions this doesn't exist, and for V5 it does. tx_extension_version: Option, } impl<'atblock, T: Config, Client: OfflineClientAtBlockT> 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 { 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>( &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 [`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<'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 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, 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(&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, marker: PhantomData, } impl<'atblock, T, Client> SubmittableTransaction<'atblock, T, Client> where T: Config, Client: OfflineClientAtBlockT, { /// Calculate and return the hash of the transaction, based on the configured hasher. pub fn hash(&self) -> HashFor { 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 { self.encoded.clone() } } impl<'atblock, T: Config, Client: OnlineClientAtBlockT> 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, 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, 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 { 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 = 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 { 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 // data layout: {weight_ref_time: Compact, weight_proof_size: Compact, class: u8, partial_fee: u128} let (_, _, _, partial_fee) = self .client .backend() .call_decoding::<(Compact, Compact, u8, u128)>( "TransactionPaymentApi_query_info", Some(¶ms), latest_block_ref.hash(), ) .await .map_err(ExtrinsicError::CannotGetFeeInfo)?; Ok(partial_fee) } }