From b85a412ecbbc22df08b9d28b3f72da2c2bbd5816 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Wed, 3 Dec 2025 16:44:05 +0000 Subject: [PATCH] Impl most transaction APIs. TxProgress and Events next --- new/src/client.rs | 12 +- new/src/client/offline_client.rs | 13 +- new/src/client/online_client.rs | 14 +- new/src/client/online_client/blocks.rs | 2 +- new/src/config.rs | 4 +- new/src/error.rs | 2 + new/src/transactions.rs | 703 +++++++++++++++++++--- new/src/transactions/account_nonce.rs | 38 ++ new/src/transactions/default_params.rs | 60 ++ new/src/transactions/validation_result.rs | 139 +++++ 10 files changed, 877 insertions(+), 110 deletions(-) create mode 100644 new/src/transactions/account_nonce.rs create mode 100644 new/src/transactions/default_params.rs create mode 100644 new/src/transactions/validation_result.rs diff --git a/new/src/client.rs b/new/src/client.rs index cfcc2eade0..2f57afbc8b 100644 --- a/new/src/client.rs +++ b/new/src/client.rs @@ -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 { +pub struct ClientAtBlock { client: Client, marker: PhantomData, } -impl ClientAtBlock { +impl ClientAtBlock { /// Construct a new client at some block. pub(crate) fn new(client: Client) -> Self { Self { @@ -26,14 +26,14 @@ impl ClientAtBlock { } } -impl ClientAtBlock +impl ClientAtBlock where T: Config, Client: OfflineClientAtBlockT, { /// Construct transactions. - pub fn tx(&self) -> Transactions<'_, T, Client> { - Transactions::new(&self.client) + pub fn tx(&self) -> Transactions { + Transactions::new(self.client.clone()) } /// Obtain a reference to the metadata. @@ -47,7 +47,7 @@ where } } -impl ClientAtBlock +impl ClientAtBlock where T: Config, Client: OnlineClientAtBlockT, diff --git a/new/src/client/offline_client.rs b/new/src/client/offline_client.rs index 62cf92bf06..04d4d759d5 100644 --- a/new/src/client/offline_client.rs +++ b/new/src/client/offline_client.rs @@ -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 OfflineClient { pub fn at_block( &self, block_number: impl Into, - ) -> Result, T>, OfflineClientAtBlockError> { + ) -> Result>, OfflineClientAtBlockError> { let block_number = block_number.into(); let (spec_version, transaction_version) = self .config @@ -35,11 +35,14 @@ impl OfflineClient { let genesis_hash = self.config.genesis_hash(); + let 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 { block_number: u64, genesis_hash: Option>, spec_version: u32, + hasher: T::Hasher, transaction_version: u32, } @@ -69,6 +73,8 @@ pub trait OfflineClientAtBlockT: Clone { fn genesis_hash(&self) -> Option>; /// 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 OfflineClientAtBlockT for OfflineClientAtBlock { fn transaction_version(&self) -> u32 { self.transaction_version } + fn hasher(&self) -> &T::Hasher { + &self.hasher + } } diff --git a/new/src/client/online_client.rs b/new/src/client/online_client.rs index 57156460f9..12e7f12a22 100644 --- a/new/src/client/online_client.rs +++ b/new/src/client/online_client.rs @@ -210,7 +210,7 @@ impl OnlineClient { /// This does not track new blocks. pub async fn at_current_block( &self, - ) -> Result, T>, OnlineClientAtBlockError> { + ) -> Result>, OnlineClientAtBlockError> { let latest_block = self .inner .backend @@ -225,7 +225,7 @@ impl OnlineClient { pub async fn at_block( &self, number_or_hash: impl Into>, - ) -> Result, T>, OnlineClientAtBlockError> { + ) -> Result>, 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 OnlineClient { &self, block_ref: impl Into>>, block_number: u64, - ) -> Result, T>, OnlineClientAtBlockError> { + ) -> Result>, OnlineClientAtBlockError> { let block_ref = block_ref.into(); let block_hash = block_ref.hash(); @@ -452,8 +452,6 @@ pub trait OnlineClientAtBlockT: OfflineClientAtBlockT { fn backend(&self) -> &dyn Backend; /// Return the block hash for the current block. fn block_hash(&self) -> HashFor; - /// 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 OnlineClientAtBlockT for OnlineClientAtBlock { fn block_hash(&self) -> HashFor { self.block_ref.hash() } - fn hasher(&self) -> &T::Hasher { - &self.hasher - } } impl OfflineClientAtBlockT for OnlineClientAtBlock { @@ -500,6 +495,9 @@ impl OfflineClientAtBlockT for OnlineClientAtBlock { fn transaction_version(&self) -> u32 { self.transaction_version } + fn hasher(&self) -> &T::Hasher { + &self.hasher + } } fn get_legacy_types<'a, T: Config, Md: ToTypeRegistry>( diff --git a/new/src/client/online_client/blocks.rs b/new/src/client/online_client/blocks.rs index aea34eede9..55e90cbfb7 100644 --- a/new/src/client/online_client/blocks.rs +++ b/new/src/client/online_client/blocks.rs @@ -70,7 +70,7 @@ impl Block { /// Instantiate a client at this block. pub async fn client( &self, - ) -> Result, T>, OnlineClientAtBlockError> { + ) -> Result>, OnlineClientAtBlockError> { self.client.at_block(self.block_ref.clone()).await } } diff --git a/new/src/config.rs b/new/src/config.rs index 06333700e9..527e509941 100644 --- a/new/src/config.rs +++ b/new/src/config.rs @@ -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<::AccountId>; + type Address: Debug + Encode + From; /// The signature type. type Signature: Debug + Clone + Encode + Decode + Send; diff --git a/new/src/error.rs b/new/src/error.rs index 9fd8d3d484..0618ea12ba 100644 --- a/new/src/error.rs +++ b/new/src/error.rs @@ -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}")] diff --git a/new/src/transactions.rs b/new/src/transactions.rs index dd5aa9c3cd..53850a8a11 100644 --- a/new/src/transactions.rs +++ b/new/src/transactions.rs @@ -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 { + client: Client, marker: PhantomData, } -impl<'atblock, T, Client> Transactions<'atblock, T, Client> { - pub(crate) fn new(client: &'atblock Client) -> Self { +impl Transactions { + 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> Transactions<'atblock, T, C> { +impl> Transactions { /// 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> 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( &self, call: &Call, - ) -> Result, ExtrinsicError> + ) -> Result, 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(&self, call: &Call) -> Result, ExtrinsicError> + pub fn create_v4_unsigned( + &self, + call: &Call, + ) -> Result, 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(&self, call: &Call) -> Result, ExtrinsicError> + pub fn create_v5_unsigned( + &self, + call: &Call, + ) -> Result, 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( + pub fn create_signable_offline( &self, call: &Call, params: >::Params, - ) -> Result, ExtrinsicError> + ) -> Result, 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> 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( + pub fn create_v4_signable_offline( &self, call: &Call, params: >::Params, - ) -> Result, ExtrinsicError> + ) -> Result, 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> 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( + pub fn create_v5_signable_offline( &self, call: &Call, params: >::Params, - ) -> Result, ExtrinsicError> + ) -> Result, 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 { + 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: u8, - ) -> Result, ExtrinsicError> { + tx_version: SupportedTransactionVersion, + ) -> Result, 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> 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> 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 { - 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( + &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)?; - 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 = + >::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> Transactions { + /// 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)] -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 { + client: 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> SignableTransaction { + /// 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 { + // 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 { + 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(&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 { + client: Client, + encoded: Vec, + marker: PhantomData, +} + +impl SubmittableTransaction +where + T: Config, + Client: OfflineClientAtBlockT, +{ + /// 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) -> 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 { + 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> SubmittableTransaction { + /// 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.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, 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 { + 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) + } } diff --git a/new/src/transactions/account_nonce.rs b/new/src/transactions/account_nonce.rs new file mode 100644 index 0000000000..60de00836f --- /dev/null +++ b/new/src/transactions/account_nonce.rs @@ -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( + client: &C, + account_id: &T::AccountId, +) -> Result +where + T: Config, + C: OnlineClientAtBlockT, +{ + 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) +} diff --git a/new/src/transactions/default_params.rs b/new/src/transactions/default_params.rs new file mode 100644 index 0000000000..51314c157e --- /dev/null +++ b/new/src/transactions/default_params.rs @@ -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 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); +}; diff --git a/new/src/transactions/validation_result.rs b/new/src/transactions/validation_result.rs new file mode 100644 index 0000000000..d421122f3e --- /dev/null +++ b/new/src/transactions/validation_result.rs @@ -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) -> Result { + // 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>, + /// 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>, + /// 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, +}