diff --git a/core/src/blocks/extrinsic_signed_extensions.rs b/core/src/blocks/extrinsic_transaction_extensions.rs similarity index 80% rename from core/src/blocks/extrinsic_signed_extensions.rs rename to core/src/blocks/extrinsic_transaction_extensions.rs index 4731cba6b0..564f2b2fc0 100644 --- a/core/src/blocks/extrinsic_signed_extensions.rs +++ b/core/src/blocks/extrinsic_transaction_extensions.rs @@ -2,10 +2,10 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -use crate::config::signed_extensions::{ +use crate::config::transaction_extensions::{ ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce, }; -use crate::config::SignedExtension; +use crate::config::TransactionExtension; use crate::dynamic::Value; use crate::{config::Config, error::Error, Metadata}; use frame_decode::extrinsics::ExtrinsicExtensions; @@ -13,14 +13,14 @@ use scale_decode::DecodeAsType; /// The signed extensions of an extrinsic. #[derive(Debug, Clone)] -pub struct ExtrinsicSignedExtensions<'a, T: Config> { +pub struct ExtrinsicTransactionExtensions<'a, T: Config> { bytes: &'a [u8], metadata: &'a Metadata, decoded_info: &'a ExtrinsicExtensions<'static, u32>, _marker: core::marker::PhantomData, } -impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> { +impl<'a, T: Config> ExtrinsicTransactionExtensions<'a, T> { pub(crate) fn new( bytes: &'a [u8], metadata: &'a Metadata, @@ -35,20 +35,22 @@ impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> { } /// Returns an iterator over each of the signed extension details of the extrinsic. - pub fn iter(&self) -> impl Iterator> { - self.decoded_info.iter().map(|s| ExtrinsicSignedExtension { - bytes: &self.bytes[s.range()], - ty_id: *s.ty(), - identifier: s.name(), - metadata: self.metadata, - _marker: core::marker::PhantomData, - }) + pub fn iter(&self) -> impl Iterator> { + self.decoded_info + .iter() + .map(|s| ExtrinsicTransactionExtension { + bytes: &self.bytes[s.range()], + ty_id: *s.ty(), + identifier: s.name(), + metadata: self.metadata, + _marker: core::marker::PhantomData, + }) } /// Searches through all signed extensions to find a specific one. /// If the Signed Extension is not found `Ok(None)` is returned. /// If the Signed Extension is found but decoding failed `Err(_)` is returned. - pub fn find>(&self) -> Result, Error> { + pub fn find>(&self) -> Result, Error> { for ext in self.iter() { match ext.as_signed_extension::() { // We found a match; return it: @@ -90,7 +92,7 @@ impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> { /// A single signed extension #[derive(Debug, Clone)] -pub struct ExtrinsicSignedExtension<'a, T: Config> { +pub struct ExtrinsicTransactionExtension<'a, T: Config> { bytes: &'a [u8], ty_id: u32, identifier: &'a str, @@ -98,7 +100,7 @@ pub struct ExtrinsicSignedExtension<'a, T: Config> { _marker: core::marker::PhantomData, } -impl<'a, T: Config> ExtrinsicSignedExtension<'a, T> { +impl<'a, T: Config> ExtrinsicTransactionExtension<'a, T> { /// The bytes representing this signed extension. pub fn bytes(&self) -> &'a [u8] { self.bytes @@ -127,7 +129,9 @@ impl<'a, T: Config> ExtrinsicSignedExtension<'a, T> { /// Decodes the bytes of this Signed Extension into its associated `Decoded` type. /// Returns `Ok(None)` if the data we have doesn't match the Signed Extension we're asking to /// decode with. - pub fn as_signed_extension>(&self) -> Result, Error> { + pub fn as_signed_extension>( + &self, + ) -> Result, Error> { if !S::matches(self.identifier, self.ty_id, self.metadata.types()) { return Ok(None); } diff --git a/core/src/blocks/extrinsics.rs b/core/src/blocks/extrinsics.rs index ac58ab87f4..0f02e0409b 100644 --- a/core/src/blocks/extrinsics.rs +++ b/core/src/blocks/extrinsics.rs @@ -3,7 +3,7 @@ // see LICENSE for license details. use super::BlockError; -use crate::blocks::extrinsic_signed_extensions::ExtrinsicSignedExtensions; +use crate::blocks::extrinsic_transaction_extensions::ExtrinsicTransactionExtensions; use crate::{ config::{Config, Hasher}, error::{Error, MetadataError}, @@ -232,17 +232,17 @@ where /// They do *not* include the `additional` signed bytes that are used as part of the payload that is signed. /// /// Note: Returns `None` if the extrinsic is not signed. - pub fn signed_extensions_bytes(&self) -> Option<&[u8]> { + pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> { self.decoded_info() .transaction_extension_payload() .map(|t| &self.bytes()[t.range()]) } /// Returns `None` if the extrinsic is not signed. - pub fn signed_extensions(&self) -> Option> { + pub fn transaction_extensions(&self) -> Option> { self.decoded_info() .transaction_extension_payload() - .map(|t| ExtrinsicSignedExtensions::new(self.bytes(), &self.metadata, t)) + .map(|t| ExtrinsicTransactionExtensions::new(self.bytes(), &self.metadata, t)) } /// The index of the pallet that the extrinsic originated from. @@ -544,7 +544,7 @@ mod tests { ); // Encoded TX ready to submit. - let tx_encoded = crate::tx::create_unsigned::(&tx, &metadata) + let tx_encoded = crate::tx::create_v4_unsigned::(&tx, &metadata) .expect("Valid dynamic parameters are provided"); // Extrinsic details ready to decode. @@ -575,7 +575,7 @@ mod tests { Value::string("SomeValue"), ], ); - let tx_encoded = crate::tx::create_unsigned::(&tx, &metadata) + let tx_encoded = crate::tx::create_v4_unsigned::(&tx, &metadata) .expect("Valid dynamic parameters are provided"); // Note: `create_unsigned` produces the extrinsic bytes by prefixing the extrinsic length. diff --git a/core/src/blocks/mod.rs b/core/src/blocks/mod.rs index 5bfc90d02d..6c3f6280c0 100644 --- a/core/src/blocks/mod.rs +++ b/core/src/blocks/mod.rs @@ -64,7 +64,7 @@ //! # ]); //! ``` -mod extrinsic_signed_extensions; +mod extrinsic_transaction_extensions; mod extrinsics; mod static_extrinsic; @@ -74,7 +74,9 @@ use crate::Metadata; use alloc::vec::Vec; pub use crate::error::BlockError; -pub use extrinsic_signed_extensions::{ExtrinsicSignedExtension, ExtrinsicSignedExtensions}; +pub use extrinsic_transaction_extensions::{ + ExtrinsicTransactionExtension, ExtrinsicTransactionExtensions, +}; pub use extrinsics::{ExtrinsicDetails, ExtrinsicMetadataDetails, Extrinsics, FoundExtrinsic}; pub use static_extrinsic::StaticExtrinsic; diff --git a/core/src/client.rs b/core/src/client.rs index 0c196ac888..db5e4fbebd 100644 --- a/core/src/client.rs +++ b/core/src/client.rs @@ -7,7 +7,7 @@ use crate::{config::Config, metadata::Metadata}; use derive_where::derive_where; -/// This provides access to some relevant client state in signed extensions, +/// This provides access to some relevant client state in transaction extensions, /// and is just a combination of some of the available properties. #[derive_where(Clone, Debug)] pub struct ClientState { diff --git a/core/src/config/default_extrinsic_params.rs b/core/src/config/default_extrinsic_params.rs index 628f639843..d12342b0ca 100644 --- a/core/src/config/default_extrinsic_params.rs +++ b/core/src/config/default_extrinsic_params.rs @@ -2,23 +2,23 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -use super::signed_extensions::CheckNonceParams; -use super::{signed_extensions, ExtrinsicParams}; -use super::{Config, Header}; +use super::Config; +use super::{transaction_extensions, ExtrinsicParams}; /// The default [`super::ExtrinsicParams`] implementation understands common signed extensions /// and how to apply them to a given chain. -pub type DefaultExtrinsicParams = signed_extensions::AnyOf< +pub type DefaultExtrinsicParams = transaction_extensions::AnyOf< T, ( - signed_extensions::CheckSpecVersion, - signed_extensions::CheckTxVersion, - signed_extensions::CheckNonce, - signed_extensions::CheckGenesis, - signed_extensions::CheckMortality, - signed_extensions::ChargeAssetTxPayment, - signed_extensions::ChargeTransactionPayment, - signed_extensions::CheckMetadataHash, + transaction_extensions::VerifySignature, + transaction_extensions::CheckSpecVersion, + transaction_extensions::CheckTxVersion, + transaction_extensions::CheckNonce, + transaction_extensions::CheckGenesis, + transaction_extensions::CheckMortality, + transaction_extensions::ChargeAssetTxPayment, + transaction_extensions::ChargeTransactionPayment, + transaction_extensions::CheckMetadataHash, ), >; @@ -26,8 +26,8 @@ pub type DefaultExtrinsicParams = signed_extensions::AnyOf< /// [`DefaultExtrinsicParams`]. This may expose methods that aren't applicable to the current /// chain; such values will simply be ignored if so. pub struct DefaultExtrinsicParamsBuilder { - /// `None` means the tx will be immortal. - mortality: Option>, + /// `None` means the tx will be immortal, else it's mortal for N blocks (if possible). + mortality: Option, /// `None` means the nonce will be automatically set. nonce: Option, /// `None` means we'll use the native token. @@ -36,16 +36,6 @@ pub struct DefaultExtrinsicParamsBuilder { tip_of: u128, } -struct Mortality { - /// Block hash that mortality starts from - checkpoint_hash: Hash, - /// Block number that mortality starts from (must - // point to the same block as the hash above) - checkpoint_number: u64, - /// How many blocks the tx is mortal for - period: u64, -} - impl Default for DefaultExtrinsicParamsBuilder { fn default() -> Self { Self { @@ -65,15 +55,10 @@ impl DefaultExtrinsicParamsBuilder { Default::default() } - /// Make the transaction mortal, given a block header that it should be mortal from, - /// and the number of blocks (roughly; it'll be rounded to a power of two) that it will - /// be mortal for. - pub fn mortal(mut self, from_block: &T::Header, for_n_blocks: u64) -> Self { - self.mortality = Some(Mortality { - checkpoint_hash: from_block.hash(), - checkpoint_number: from_block.number().into(), - period: for_n_blocks, - }); + /// Make the transaction mortal, given a number of blocks it will be mortal for from + /// the current block at the time of submission. + pub fn mortal(mut self, for_n_blocks: u64) -> Self { + self.mortality = Some(for_n_blocks); self } @@ -83,26 +68,6 @@ impl DefaultExtrinsicParamsBuilder { self } - /// Make the transaction mortal, given a block number and block hash (which must both point to - /// the same block) that it should be mortal from, and the number of blocks (roughly; it'll be - /// rounded to a power of two) that it will be mortal for. - /// - /// Prefer to use [`DefaultExtrinsicParamsBuilder::mortal()`], which ensures that the block hash - /// and number align. - pub fn mortal_unchecked( - mut self, - from_block_number: u64, - from_block_hash: T::Hash, - for_n_blocks: u64, - ) -> Self { - self.mortality = Some(Mortality { - checkpoint_hash: from_block_hash, - checkpoint_number: from_block_number, - period: for_n_blocks, - }); - self - } - /// Provide a tip to the block author in the chain's native token. pub fn tip(mut self, tip: u128) -> Self { self.tip = tip; @@ -123,28 +88,29 @@ impl DefaultExtrinsicParamsBuilder { /// Build the extrinsic parameters. pub fn build(self) -> as ExtrinsicParams>::Params { - let check_mortality_params = if let Some(mortality) = self.mortality { - signed_extensions::CheckMortalityParams::mortal( - mortality.period, - mortality.checkpoint_number, - mortality.checkpoint_hash, - ) + let check_mortality_params = if let Some(for_n_blocks) = self.mortality { + transaction_extensions::CheckMortalityParams::mortal(for_n_blocks) } else { - signed_extensions::CheckMortalityParams::immortal() + transaction_extensions::CheckMortalityParams::immortal() }; let charge_asset_tx_params = if let Some(asset_id) = self.tip_of_asset_id { - signed_extensions::ChargeAssetTxPaymentParams::tip_of(self.tip, asset_id) + transaction_extensions::ChargeAssetTxPaymentParams::tip_of(self.tip, asset_id) } else { - signed_extensions::ChargeAssetTxPaymentParams::tip(self.tip) + transaction_extensions::ChargeAssetTxPaymentParams::tip(self.tip) }; let charge_transaction_params = - signed_extensions::ChargeTransactionPaymentParams::tip(self.tip); + transaction_extensions::ChargeTransactionPaymentParams::tip(self.tip); - let check_nonce_params = CheckNonceParams(self.nonce); + let check_nonce_params = if let Some(nonce) = self.nonce { + transaction_extensions::CheckNonceParams::with_nonce(nonce) + } else { + transaction_extensions::CheckNonceParams::from_chain() + }; ( + (), (), (), check_nonce_params, @@ -156,3 +122,16 @@ impl DefaultExtrinsicParamsBuilder { ) } } + +#[cfg(test)] +mod test { + use super::*; + + fn assert_default(_t: T) {} + + #[test] + fn params_are_default() { + let params = DefaultExtrinsicParamsBuilder::::new().build(); + assert_default(params) + } +} diff --git a/core/src/config/extrinsic_params.rs b/core/src/config/extrinsic_params.rs index 1353ec3a25..89b926aa06 100644 --- a/core/src/config/extrinsic_params.rs +++ b/core/src/config/extrinsic_params.rs @@ -7,9 +7,9 @@ //! [`crate::config::DefaultExtrinsicParams`] provides a general-purpose //! implementation of this that will work in many cases. -use super::refine_params::RefineParams; use crate::{client::ClientState, error::ExtrinsicParamsError, Config}; use alloc::vec::Vec; +use core::any::Any; /// This trait allows you to configure the "signed extra" and /// "additional" parameters that are a part of the transaction payload @@ -18,7 +18,7 @@ pub trait ExtrinsicParams: ExtrinsicParamsEncoder + Sized + Send + 's /// These parameters can be provided to the constructor along with /// some default parameters that `subxt` understands, in order to /// help construct your [`ExtrinsicParams`] object. - type Params: RefineParams; + type Params: Params; /// Construct a new instance of our [`ExtrinsicParams`]. fn new(client: &ClientState, params: Self::Params) -> Result; @@ -28,15 +28,92 @@ pub trait ExtrinsicParams: ExtrinsicParamsEncoder + Sized + Send + 's /// defines how to encode the "additional" and "extra" params. Both functions /// are optional and will encode nothing by default. pub trait ExtrinsicParamsEncoder: 'static { - /// This is expected to SCALE encode the "signed extra" parameters - /// to some buffer that has been provided. These are the parameters - /// which are sent along with the transaction, as well as taken into - /// account when signing the transaction. - fn encode_extra_to(&self, _v: &mut Vec) {} + /// This is expected to SCALE encode the transaction extension data to some + /// buffer that has been provided. This data is attached to the transaction + /// and also (by default) attached to the signer payload which is signed to + /// provide a signature for the transaction. + /// + /// If [`ExtrinsicParamsEncoder::encode_signer_payload_value_to`] is implemented, + /// then that will be used instead when generating a signer payload. Useful for + /// eg the `VerifySignature` extension, which is send with the transaction but + /// is not a part of the signer payload. + fn encode_value_to(&self, _v: &mut Vec) {} - /// This is expected to SCALE encode the "additional" parameters - /// to some buffer that has been provided. These parameters are _not_ - /// sent along with the transaction, but are taken into account when + /// See [`ExtrinsicParamsEncoder::encode_value_to`]. This defaults to calling that + /// method, but if implemented will dictate what is encoded to the signer payload. + fn encode_signer_payload_value_to(&self, v: &mut Vec) { + self.encode_value_to(v); + } + + /// This is expected to SCALE encode the "implicit" (formally "additional") + /// parameters to some buffer that has been provided. These parameters are + /// _not_ sent along with the transaction, but are taken into account when /// signing it, meaning the client and node must agree on their values. - fn encode_additional_to(&self, _v: &mut Vec) {} + fn encode_implicit_to(&self, _v: &mut Vec) {} + + /// Set the signature. This happens after we have constructed the extrinsic params, + /// and so is defined here rather than on the params, below. We need to use `&dyn Any` + /// to keep this trait object safe, but can downcast in the impls. + /// + /// # Panics + /// + /// Implementations of this will likely try to downcast the provided `account_id` + /// and `signature` into `T::AccountId` and `T::Signature` (where `T: Config`), and are + /// free to panic if this downcasting does not succeed. + /// + /// In typical usage, this is not a problem, since this method is only called internally + /// and provided values which line up with the relevant `Config`. In theory though, this + /// method can be called manually with any types, hence this warning. + fn inject_signature(&mut self, _account_id: &dyn Any, _signature: &dyn Any) {} } + +/// The parameters (ie [`ExtrinsicParams::Params`]) can also have data injected into them, +/// allowing Subxt to retrieve data from the chain and amend the parameters with it when +/// online. +pub trait Params { + /// Set the account nonce. + fn inject_account_nonce(&mut self, _nonce: u64) {} + /// Set the current block. + fn inject_block(&mut self, _number: u64, _hash: T::Hash) {} +} + +impl Params for () {} + +macro_rules! impl_tuples { + ($($ident:ident $index:tt),+) => { + + impl ),+> Params for ($($ident,)+){ + fn inject_account_nonce(&mut self, nonce: u64) { + $(self.$index.inject_account_nonce(nonce);)+ + } + fn inject_block(&mut self, number: u64, hash: T::Hash) { + $(self.$index.inject_block(number, hash);)+ + } + } + } +} + +#[rustfmt::skip] +const _: () = { + impl_tuples!(A 0); + impl_tuples!(A 0, B 1); + impl_tuples!(A 0, B 1, C 2); + impl_tuples!(A 0, B 1, C 2, D 3); + impl_tuples!(A 0, B 1, C 2, D 3, E 4); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19, V 20); +}; diff --git a/core/src/config/mod.rs b/core/src/config/mod.rs index e171106750..2c0187365e 100644 --- a/core/src/config/mod.rs +++ b/core/src/config/mod.rs @@ -10,11 +10,10 @@ mod default_extrinsic_params; mod extrinsic_params; -mod refine_params; pub mod polkadot; -pub mod signed_extensions; pub mod substrate; +pub mod transaction_extensions; use codec::{Decode, Encode}; use core::fmt::Debug; @@ -25,9 +24,8 @@ use serde::{de::DeserializeOwned, Serialize}; pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder}; pub use extrinsic_params::{ExtrinsicParams, ExtrinsicParamsEncoder}; pub use polkadot::{PolkadotConfig, PolkadotExtrinsicParams, PolkadotExtrinsicParamsBuilder}; -pub use refine_params::{RefineParams, RefineParamsData}; -pub use signed_extensions::SignedExtension; pub use substrate::{SubstrateConfig, SubstrateExtrinsicParams, SubstrateExtrinsicParamsBuilder}; +pub use transaction_extensions::TransactionExtension; /// Runtime types. // Note: the `Send + Sync + 'static` bound isn't strictly required, but currently deriving @@ -39,13 +37,13 @@ pub trait Config: Sized + Send + Sync + 'static { type Hash: BlockHash; /// The account ID type. - type AccountId: Debug + Clone + Encode + Serialize; + type AccountId: Debug + Clone + Encode + Decode + Serialize + Send; /// The address type. type Address: Debug + Encode + From; /// The signature type. - type Signature: Debug + Encode; + type Signature: Debug + Clone + Encode + Decode + Send; /// The hashing system (algorithm) being used in the runtime (e.g. Blake2). type Hasher: Debug + Hasher; diff --git a/core/src/config/refine_params.rs b/core/src/config/refine_params.rs deleted file mode 100644 index eaf7b44450..0000000000 --- a/core/src/config/refine_params.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2019-2024 Parity Technologies (UK) Ltd. -// This file is dual-licensed as Apache-2.0 or GPL-3.0. -// see LICENSE for license details. - -//! Refining params with values fetched from the chain - -use crate::Config; - -/// Data that can be used to refine the params of signed extensions. -pub struct RefineParamsData { - account_nonce: u64, - block_number: u64, - block_hash: T::Hash, -} - -impl RefineParamsData { - #[doc(hidden)] - /// Creates a new [`RefineParamsData`] instance. Called from `subxt` when refining signed extensions. - pub fn new(account_nonce: u64, block_number: u64, block_hash: T::Hash) -> Self { - RefineParamsData { - account_nonce, - block_number, - block_hash, - } - } - - /// account nonce for extrinsic author - pub fn account_nonce(&self) -> u64 { - self.account_nonce - } - - /// latest finalized block number - pub fn block_number(&self) -> u64 { - self.block_number - } - - /// latest finalized block hash - pub fn block_hash(&self) -> T::Hash { - self.block_hash - } -} - -/// Types implementing [`RefineParams`] can be modified to reflect live information from the chain. -pub trait RefineParams { - /// Refine params to an extrinsic. There is usually some notion of 'the param is already set/unset' in types implementing this trait. - /// The refinement should most likely not affect cases where a param is in a 'is already set by the user' state. - fn refine(&mut self, _data: &RefineParamsData) {} -} - -impl RefineParams for () {} - -macro_rules! impl_tuples { - ($($ident:ident $index:tt),+) => { - - impl ),+> RefineParams for ($($ident,)+){ - fn refine(&mut self, data: &RefineParamsData) { - $(self.$index.refine(data);)+ - } - - } - } -} - -#[rustfmt::skip] -const _: () = { - impl_tuples!(A 0); - impl_tuples!(A 0, B 1); - impl_tuples!(A 0, B 1, C 2); - impl_tuples!(A 0, B 1, C 2, D 3); - impl_tuples!(A 0, B 1, C 2, D 3, E 4); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19, V 20); -}; diff --git a/core/src/config/signed_extensions.rs b/core/src/config/transaction_extensions.rs similarity index 56% rename from core/src/config/signed_extensions.rs rename to core/src/config/transaction_extensions.rs index 161a255de5..529ed94109 100644 --- a/core/src/config/signed_extensions.rs +++ b/core/src/config/transaction_extensions.rs @@ -2,45 +2,116 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -//! This module contains implementations for common signed extensions, each -//! of which implements [`SignedExtension`], and can be used in conjunction with -//! [`AnyOf`] to configure the set of signed extensions which are known about +//! This module contains implementations for common transaction extensions, each +//! of which implements [`TransactionExtension`], and can be used in conjunction with +//! [`AnyOf`] to configure the set of transaction extensions which are known about //! when interacting with a chain. use super::extrinsic_params::ExtrinsicParams; -use super::refine_params::RefineParamsData; -use super::RefineParams; use crate::client::ClientState; -use crate::config::ExtrinsicParamsEncoder; +use crate::config::{ExtrinsicParamsEncoder, Header}; use crate::error::ExtrinsicParamsError; -use crate::utils::Era; +use crate::utils::{Era, Static}; use crate::Config; use alloc::borrow::ToOwned; use alloc::boxed::Box; use alloc::vec::Vec; use codec::{Compact, Encode}; +use core::any::Any; use core::fmt::Debug; use derive_where::derive_where; use hashbrown::HashMap; use scale_decode::DecodeAsType; use scale_info::PortableRegistry; -/// A single [`SignedExtension`] has a unique name, but is otherwise the +// Re-export this here; it's a bit generically named to be re-exported from ::config. +pub use super::extrinsic_params::Params; + +/// A single [`TransactionExtension`] has a unique name, but is otherwise the /// same as [`ExtrinsicParams`] in describing how to encode the extra and /// additional data. -pub trait SignedExtension: ExtrinsicParams { - /// The type representing the `extra` bytes of a signed extension. +pub trait TransactionExtension: ExtrinsicParams { + /// The type representing the `extra` / value bytes of a transaction extension. /// Decoding from this type should be symmetrical to the respective - /// `ExtrinsicParamsEncoder::encode_extra_to()` implementation of this signed extension. + /// `ExtrinsicParamsEncoder::encode_value_to()` implementation of this transaction extension. type Decoded: DecodeAsType; - /// This should return true if the signed extension matches the details given. + /// This should return true if the transaction extension matches the details given. /// Often, this will involve just checking that the identifier given matches that of the /// extension in question. fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool; } -/// The [`CheckMetadataHash`] signed extension. +/// The [`VerifySignature`] extension. For V5 General transactions, this is how a signature +/// is provided. The signature is constructed by signing a payload which contains the +/// transaction call data as well as the encoded "additional" bytes for any extensions _after_ +/// this one in the list. +pub struct VerifySignature(VerifySignatureDetails); + +impl ExtrinsicParams for VerifySignature { + type Params = (); + + fn new(_client: &ClientState, _params: Self::Params) -> Result { + Ok(VerifySignature(VerifySignatureDetails::Disabled)) + } +} + +impl ExtrinsicParamsEncoder for VerifySignature { + fn encode_value_to(&self, v: &mut Vec) { + self.0.encode_to(v); + } + fn encode_signer_payload_value_to(&self, v: &mut Vec) { + // This extension is never encoded to the signer payload, and extensions + // prior to this are ignored when creating said payload, so clear anything + // we've seen so far. + v.clear(); + } + fn encode_implicit_to(&self, v: &mut Vec) { + // We only use the "implicit" data for extensions _after_ this one + // in the pipeline to form the signer payload. Thus, clear anything + // we've seen so far. + v.clear(); + } + + fn inject_signature(&mut self, account: &dyn Any, signature: &dyn Any) { + // Downcast refs back to concrete types (we use `&dyn Any`` so that the trait remains object safe) + let account = account + .downcast_ref::() + .expect("A T::AccoundId should have been provided") + .clone(); + let signature = signature + .downcast_ref::() + .expect("A T::Signature should have been provided") + .clone(); + + // The signature is not set through params, only here, once given by a user: + self.0 = VerifySignatureDetails::Signed { signature, account } + } +} + +impl TransactionExtension for VerifySignature { + type Decoded = Static>; + fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { + identifier == "VerifySignature" + } +} + +/// This allows a signature to be provided to the [`VerifySignature`] transaction extension. +// Dev note: this must encode identically to https://github.com/paritytech/polkadot-sdk/blob/fd72d58313c297a10600037ce1bb88ec958d722e/substrate/frame/verify-signature/src/extension.rs#L43 +#[derive(codec::Encode, codec::Decode)] +pub enum VerifySignatureDetails { + /// A signature has been provided. + Signed { + /// The signature. + signature: T::Signature, + /// The account that generated the signature. + account: T::AccountId, + }, + /// No signature was provided. + Disabled, +} + +/// The [`CheckMetadataHash`] transaction extension. pub struct CheckMetadataHash { // Eventually we might provide or calculate the metadata hash here, // but for now we never provide a hash and so this is empty. @@ -55,18 +126,18 @@ impl ExtrinsicParams for CheckMetadataHash { } impl ExtrinsicParamsEncoder for CheckMetadataHash { - fn encode_extra_to(&self, v: &mut Vec) { + fn encode_value_to(&self, v: &mut Vec) { // A single 0 byte in the TX payload indicates that the chain should // _not_ expect any metadata hash to exist in the signer payload. 0u8.encode_to(v); } - fn encode_additional_to(&self, v: &mut Vec) { + fn encode_implicit_to(&self, v: &mut Vec) { // We provide no metadata hash in the signer payload to align with the above. None::<()>.encode_to(v); } } -impl SignedExtension for CheckMetadataHash { +impl TransactionExtension for CheckMetadataHash { type Decoded = CheckMetadataHashMode; fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { identifier == "CheckMetadataHash" @@ -75,7 +146,7 @@ impl SignedExtension for CheckMetadataHash { /// Is metadata checking enabled or disabled? // Dev note: The "Disabled" and "Enabled" variant names match those that the -// signed extension will be encoded with, in order that DecodeAsType will work +// transaction extension will be encoded with, in order that DecodeAsType will work // properly. #[derive(Copy, Clone, Debug, DecodeAsType)] pub enum CheckMetadataHashMode { @@ -95,7 +166,7 @@ impl CheckMetadataHashMode { } } -/// The [`CheckSpecVersion`] signed extension. +/// The [`CheckSpecVersion`] transaction extension. pub struct CheckSpecVersion(u32); impl ExtrinsicParams for CheckSpecVersion { @@ -107,57 +178,66 @@ impl ExtrinsicParams for CheckSpecVersion { } impl ExtrinsicParamsEncoder for CheckSpecVersion { - fn encode_additional_to(&self, v: &mut Vec) { + fn encode_implicit_to(&self, v: &mut Vec) { self.0.encode_to(v); } } -impl SignedExtension for CheckSpecVersion { +impl TransactionExtension for CheckSpecVersion { type Decoded = (); fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { identifier == "CheckSpecVersion" } } -/// The [`CheckNonce`] signed extension. -pub struct CheckNonce(Compact); +/// The [`CheckNonce`] transaction extension. +pub struct CheckNonce(u64); impl ExtrinsicParams for CheckNonce { type Params = CheckNonceParams; fn new(_client: &ClientState, params: Self::Params) -> Result { - // If no nonce is set (nor by user nor refinement), use a nonce of 0. - let nonce = params.0.unwrap_or(0); - Ok(CheckNonce(Compact(nonce))) + Ok(CheckNonce(params.0.unwrap_or(0))) } } impl ExtrinsicParamsEncoder for CheckNonce { - fn encode_extra_to(&self, v: &mut Vec) { - self.0.encode_to(v); + fn encode_value_to(&self, v: &mut Vec) { + Compact(self.0).encode_to(v); } } -impl SignedExtension for CheckNonce { +impl TransactionExtension for CheckNonce { type Decoded = u64; fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { identifier == "CheckNonce" } } -/// Params for [`CheckNonce`] +/// Configure the nonce used. #[derive(Debug, Clone, Default)] -pub struct CheckNonceParams(pub Option); +pub struct CheckNonceParams(Option); -impl RefineParams for CheckNonceParams { - fn refine(&mut self, data: &RefineParamsData) { +impl CheckNonceParams { + /// Retrieve the nonce from the chain and use that. + pub fn from_chain() -> Self { + Self(None) + } + /// Manually set an account nonce to use. + pub fn with_nonce(nonce: u64) -> Self { + Self(Some(nonce)) + } +} + +impl Params for CheckNonceParams { + fn inject_account_nonce(&mut self, nonce: u64) { if self.0.is_none() { - self.0 = Some(data.account_nonce()); + self.0 = Some(nonce) } } } -/// The [`CheckTxVersion`] signed extension. +/// The [`CheckTxVersion`] transaction extension. pub struct CheckTxVersion(u32); impl ExtrinsicParams for CheckTxVersion { @@ -169,19 +249,19 @@ impl ExtrinsicParams for CheckTxVersion { } impl ExtrinsicParamsEncoder for CheckTxVersion { - fn encode_additional_to(&self, v: &mut Vec) { + fn encode_implicit_to(&self, v: &mut Vec) { self.0.encode_to(v); } } -impl SignedExtension for CheckTxVersion { +impl TransactionExtension for CheckTxVersion { type Decoded = (); fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { identifier == "CheckTxVersion" } } -/// The [`CheckGenesis`] signed extension. +/// The [`CheckGenesis`] transaction extension. pub struct CheckGenesis(T::Hash); impl ExtrinsicParams for CheckGenesis { @@ -193,104 +273,149 @@ impl ExtrinsicParams for CheckGenesis { } impl ExtrinsicParamsEncoder for CheckGenesis { - fn encode_additional_to(&self, v: &mut Vec) { + fn encode_implicit_to(&self, v: &mut Vec) { self.0.encode_to(v); } } -impl SignedExtension for CheckGenesis { +impl TransactionExtension for CheckGenesis { type Decoded = (); fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { identifier == "CheckGenesis" } } -/// The [`CheckMortality`] signed extension. +/// The [`CheckMortality`] transaction extension. pub struct CheckMortality { - era: Era, - checkpoint: T::Hash, -} - -/// Parameters to configure the [`CheckMortality`] signed extension. -pub struct CheckMortalityParams(Option>); -struct CheckMortalityParamsInner { - era: Era, - checkpoint: Option, -} - -impl Default for CheckMortalityParams { - fn default() -> Self { - CheckMortalityParams(None) - } -} - -impl RefineParams for CheckMortalityParams { - fn refine(&mut self, data: &RefineParamsData) { - if self.0.is_none() { - // By default we refine the params to have a mortal transaction valid for 32 blocks. - const TX_VALID_FOR: u64 = 32; - *self = - CheckMortalityParams::mortal(TX_VALID_FOR, data.block_number(), data.block_hash()); - } - } -} - -impl CheckMortalityParams { - /// Configure a mortal transaction. The `period` is (roughly) how many - /// blocks the transaction will be valid for. The `block_number` and - /// `block_hash` should both point to the same block, and are the block that - /// the transaction is mortal from. - pub fn mortal(period: u64, block_number: u64, block_hash: T::Hash) -> Self { - Self(Some(CheckMortalityParamsInner { - era: Era::mortal(period, block_number), - checkpoint: Some(block_hash), - })) - } - /// An immortal transaction. - pub fn immortal() -> Self { - Self(Some(CheckMortalityParamsInner { - era: Era::Immortal, - checkpoint: None, - })) - } + params: CheckMortalityParamsInner, + genesis_hash: T::Hash, } impl ExtrinsicParams for CheckMortality { type Params = CheckMortalityParams; fn new(client: &ClientState, params: Self::Params) -> Result { - let check_mortality = if let Some(params) = params.0 { - CheckMortality { - era: params.era, - checkpoint: params.checkpoint.unwrap_or(client.genesis_hash), - } - } else { - CheckMortality { - era: Era::Immortal, - checkpoint: client.genesis_hash, - } - }; - Ok(check_mortality) + Ok(CheckMortality { + // if nothing has been explicitly configured, we will have a mortal transaction + // valid for 32 blocks if block info is available. + params: params.0, + genesis_hash: client.genesis_hash, + }) } } impl ExtrinsicParamsEncoder for CheckMortality { - fn encode_extra_to(&self, v: &mut Vec) { - self.era.encode_to(v); + fn encode_value_to(&self, v: &mut Vec) { + match &self.params { + CheckMortalityParamsInner::MortalFromBlock { + for_n_blocks, + from_block_n, + .. + } => { + Era::mortal(*for_n_blocks, *from_block_n).encode_to(v); + } + _ => { + // Note: if we see `CheckMortalityInner::MortalForBlocks`, then it means the user has + // configured a block to be mortal for N blocks, but the current block was never injected, + // so we don't know where to start from and default back to building an immortal tx. + Era::Immortal.encode_to(v); + } + } } - fn encode_additional_to(&self, v: &mut Vec) { - self.checkpoint.encode_to(v); + fn encode_implicit_to(&self, v: &mut Vec) { + match &self.params { + CheckMortalityParamsInner::MortalFromBlock { + from_block_hash, .. + } => { + from_block_hash.encode_to(v); + } + _ => { + self.genesis_hash.encode_to(v); + } + } } } -impl SignedExtension for CheckMortality { +impl TransactionExtension for CheckMortality { type Decoded = Era; fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { identifier == "CheckMortality" } } -/// The [`ChargeAssetTxPayment`] signed extension. +/// Parameters to configure the [`CheckMortality`] transaction extension. +pub struct CheckMortalityParams(CheckMortalityParamsInner); + +enum CheckMortalityParamsInner { + Immortal, + MortalForBlocks(u64), + MortalFromBlock { + for_n_blocks: u64, + from_block_n: u64, + from_block_hash: T::Hash, + }, +} + +impl Default for CheckMortalityParams { + fn default() -> Self { + // default to being mortal for 32 blocks if possible: + CheckMortalityParams(CheckMortalityParamsInner::MortalForBlocks(32)) + } +} + +impl CheckMortalityParams { + /// Configure a transaction that will be mortal for the number of blocks given. + pub fn mortal(for_n_blocks: u64) -> Self { + Self(CheckMortalityParamsInner::MortalForBlocks(for_n_blocks)) + } + /// Configure a transaction that will be mortal for the number of blocks given, + /// and from the block header provided. + pub fn mortal_from(for_n_blocks: u64, from_block: T::Header) -> Self { + Self(CheckMortalityParamsInner::MortalFromBlock { + for_n_blocks, + from_block_n: from_block.number().into(), + from_block_hash: from_block.hash(), + }) + } + /// Configure a transaction that will be mortal for the number of blocks given, + /// and from the block details provided. Prefer to use [`CheckMortalityParams::mortal()`] + /// or [`CheckMortalityParams::mortal_from()`] which both avoid the block number and hash + /// from being misaligned. + pub fn mortal_from_unchecked( + for_n_blocks: u64, + from_block_n: u64, + from_block_hash: T::Hash, + ) -> Self { + Self(CheckMortalityParamsInner::MortalFromBlock { + for_n_blocks, + from_block_n, + from_block_hash, + }) + } + /// An immortal transaction. + pub fn immortal() -> Self { + Self(CheckMortalityParamsInner::Immortal) + } +} + +impl Params for CheckMortalityParams { + fn inject_block(&mut self, from_block_n: u64, from_block_hash: ::Hash) { + match &self.0 { + CheckMortalityParamsInner::MortalForBlocks(n) => { + self.0 = CheckMortalityParamsInner::MortalFromBlock { + for_n_blocks: *n, + from_block_n, + from_block_hash, + } + } + _ => { + // Don't change anything if explicit Immortal or explicit block set. + } + } + } +} + +/// The [`ChargeAssetTxPayment`] transaction extension. #[derive(DecodeAsType)] #[derive_where(Clone, Debug; T::AssetId)] #[decode_as_type(trait_bounds = "T::AssetId: DecodeAsType")] @@ -311,7 +436,31 @@ impl ChargeAssetTxPayment { } } -/// Parameters to configure the [`ChargeAssetTxPayment`] signed extension. +impl ExtrinsicParams for ChargeAssetTxPayment { + type Params = ChargeAssetTxPaymentParams; + + fn new(_client: &ClientState, params: Self::Params) -> Result { + Ok(ChargeAssetTxPayment { + tip: Compact(params.tip), + asset_id: params.asset_id, + }) + } +} + +impl ExtrinsicParamsEncoder for ChargeAssetTxPayment { + fn encode_value_to(&self, v: &mut Vec) { + (self.tip, &self.asset_id).encode_to(v); + } +} + +impl TransactionExtension for ChargeAssetTxPayment { + type Decoded = Self; + fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { + identifier == "ChargeAssetTxPayment" + } +} + +/// Parameters to configure the [`ChargeAssetTxPayment`] transaction extension. pub struct ChargeAssetTxPaymentParams { tip: u128, asset_id: Option, @@ -350,33 +499,9 @@ impl ChargeAssetTxPaymentParams { } } -impl ExtrinsicParams for ChargeAssetTxPayment { - type Params = ChargeAssetTxPaymentParams; +impl Params for ChargeAssetTxPaymentParams {} - fn new(_client: &ClientState, params: Self::Params) -> Result { - Ok(ChargeAssetTxPayment { - tip: Compact(params.tip), - asset_id: params.asset_id, - }) - } -} - -impl RefineParams for ChargeAssetTxPaymentParams {} - -impl ExtrinsicParamsEncoder for ChargeAssetTxPayment { - fn encode_extra_to(&self, v: &mut Vec) { - (self.tip, &self.asset_id).encode_to(v); - } -} - -impl SignedExtension for ChargeAssetTxPayment { - type Decoded = Self; - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "ChargeAssetTxPayment" - } -} - -/// The [`ChargeTransactionPayment`] signed extension. +/// The [`ChargeTransactionPayment`] transaction extension. #[derive(Clone, Debug, DecodeAsType)] pub struct ChargeTransactionPayment { tip: Compact, @@ -389,7 +514,30 @@ impl ChargeTransactionPayment { } } -/// Parameters to configure the [`ChargeTransactionPayment`] signed extension. +impl ExtrinsicParams for ChargeTransactionPayment { + type Params = ChargeTransactionPaymentParams; + + fn new(_client: &ClientState, params: Self::Params) -> Result { + Ok(ChargeTransactionPayment { + tip: Compact(params.tip), + }) + } +} + +impl ExtrinsicParamsEncoder for ChargeTransactionPayment { + fn encode_value_to(&self, v: &mut Vec) { + self.tip.encode_to(v); + } +} + +impl TransactionExtension for ChargeTransactionPayment { + type Decoded = Self; + fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { + identifier == "ChargeTransactionPayment" + } +} + +/// Parameters to configure the [`ChargeTransactionPayment`] transaction extension. #[derive(Default)] pub struct ChargeTransactionPaymentParams { tip: u128, @@ -406,32 +554,9 @@ impl ChargeTransactionPaymentParams { } } -impl ExtrinsicParams for ChargeTransactionPayment { - type Params = ChargeTransactionPaymentParams; +impl Params for ChargeTransactionPaymentParams {} - fn new(_client: &ClientState, params: Self::Params) -> Result { - Ok(ChargeTransactionPayment { - tip: Compact(params.tip), - }) - } -} - -impl RefineParams for ChargeTransactionPaymentParams {} - -impl ExtrinsicParamsEncoder for ChargeTransactionPayment { - fn encode_extra_to(&self, v: &mut Vec) { - self.tip.encode_to(v); - } -} - -impl SignedExtension for ChargeTransactionPayment { - type Decoded = Self; - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "ChargeTransactionPayment" - } -} - -/// This accepts a tuple of [`SignedExtension`]s, and will dynamically make use of whichever +/// This accepts a tuple of [`TransactionExtension`]s, and will dynamically make use of whichever /// ones are actually required for the chain in the correct order, ignoring the rest. This /// is a sensible default, and allows for a single configuration to work across multiple chains. pub struct AnyOf { @@ -447,7 +572,7 @@ macro_rules! impl_tuples { impl ExtrinsicParams for AnyOf where T: Config, - $($ident: SignedExtension,)+ + $($ident: TransactionExtension,)+ { type Params = ($($ident::Params,)+); @@ -458,11 +583,11 @@ macro_rules! impl_tuples { let metadata = &client.metadata; let types = metadata.types(); - // For each signed extension in the tuple, find the matching index in the metadata, if + // For each transaction extension in the tuple, find the matching index in the metadata, if // there is one, and add it to a map with that index as the key. let mut exts_by_index = HashMap::new(); $({ - for (idx, e) in metadata.extrinsic().signed_extensions().iter().enumerate() { + for (idx, e) in metadata.extrinsic().transaction_extensions().iter().enumerate() { // Skip over any exts that have a match already: if exts_by_index.contains_key(&idx) { continue @@ -479,12 +604,12 @@ macro_rules! impl_tuples { // Next, turn these into an ordered vec, erroring if we haven't matched on any exts yet. let mut params = Vec::new(); - for (idx, e) in metadata.extrinsic().signed_extensions().iter().enumerate() { + for (idx, e) in metadata.extrinsic().transaction_extensions().iter().enumerate() { let Some(ext) = exts_by_index.remove(&idx) else { if is_type_empty(e.extra_ty(), types) { continue } else { - return Err(ExtrinsicParamsError::UnknownSignedExtension(e.identifier().to_owned())); + return Err(ExtrinsicParamsError::UnknownTransactionExtension(e.identifier().to_owned())); } }; params.push(ext); @@ -500,16 +625,26 @@ macro_rules! impl_tuples { impl ExtrinsicParamsEncoder for AnyOf where T: Config, - $($ident: SignedExtension,)+ + $($ident: TransactionExtension,)+ { - fn encode_extra_to(&self, v: &mut Vec) { + fn encode_value_to(&self, v: &mut Vec) { for ext in &self.params { - ext.encode_extra_to(v); + ext.encode_value_to(v); } } - fn encode_additional_to(&self, v: &mut Vec) { + fn encode_signer_payload_value_to(&self, v: &mut Vec) { for ext in &self.params { - ext.encode_additional_to(v); + ext.encode_signer_payload_value_to(v); + } + } + fn encode_implicit_to(&self, v: &mut Vec) { + for ext in &self.params { + ext.encode_implicit_to(v); + } + } + fn inject_signature(&mut self, account_id: &dyn Any, signature: &dyn Any) { + for ext in &mut self.params { + ext.inject_signature(account_id, signature); } } } diff --git a/core/src/error.rs b/core/src/error.rs index 86362125e7..5e5724537a 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -27,9 +27,9 @@ pub enum Error { /// Error encoding from a [`crate::dynamic::Value`]. #[error("Error encoding from dynamic value: {0}")] Encode(#[from] scale_encode::Error), - /// Error constructing the appropriate extrinsic params. - #[error(transparent)] - ExtrinsicParams(#[from] ExtrinsicParamsError), + /// Error constructing an extrinsic. + #[error("Error constructing transaction: {0}")] + Extrinsic(#[from] ExtrinsicError), /// Block body error. #[error("Error working with block_body: {0}")] Block(#[from] BlockError), @@ -162,6 +162,24 @@ pub enum StorageAddressError { }, } +/// An error that can be encountered when constructing a transaction. +#[derive(Debug, DeriveError)] +#[non_exhaustive] +pub enum ExtrinsicError { + /// Transaction version not supported by Subxt. + #[error("Subxt does not support the extrinsic versions expected by the chain")] + UnsupportedVersion, + /// Issue encoding transaction extensions. + #[error("Cannot construct the required transaction extensions: {0}")] + Params(#[from] ExtrinsicParamsError), +} + +impl From for Error { + fn from(value: ExtrinsicParamsError) -> Self { + Error::Extrinsic(value.into()) + } +} + /// An error that can be emitted when trying to construct an instance of [`crate::config::ExtrinsicParams`], /// encode data from the instance, or match on signed extensions. #[derive(Debug, DeriveError)] @@ -178,7 +196,7 @@ pub enum ExtrinsicParamsError { }, /// A signed extension in use on some chain was not provided. #[error("The chain expects a signed extension with the name {0}, but we did not provide one")] - UnknownSignedExtension(String), + UnknownTransactionExtension(String), /// Some custom error. #[error("Error constructing extrinsic parameters: {0}")] Custom(Box), diff --git a/core/src/tx/mod.rs b/core/src/tx/mod.rs index 4bf61bfb09..664b28dced 100644 --- a/core/src/tx/mod.rs +++ b/core/src/tx/mod.rs @@ -48,7 +48,9 @@ //! tx::validate(&call, &state.metadata).unwrap(); //! //! // We can build a signed transaction: -//! let signed_call = tx::create_signed(&call, &state, &dev::alice(), params).unwrap(); +//! let signed_call = tx::create_v4_signed(&call, &state, params) +//! .unwrap() +//! .sign(&dev::alice()); //! //! // And log it: //! println!("Tx: 0x{}", hex::encode(signed_call.encoded())); @@ -58,7 +60,7 @@ pub mod payload; pub mod signer; use crate::config::{Config, ExtrinsicParams, ExtrinsicParamsEncoder, Hasher}; -use crate::error::{Error, MetadataError}; +use crate::error::{Error, ExtrinsicError, MetadataError}; use crate::metadata::Metadata; use crate::utils::Encoded; use alloc::borrow::{Cow, ToOwned}; @@ -89,6 +91,32 @@ pub fn validate(call: &Call, metadata: &Metadata) -> Result<(), E Ok(()) } +/// 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 suggested_version(metadata: &Metadata) -> Result { + let versions = metadata.extrinsic().supported_versions(); + + if versions.contains(&4) { + Ok(TransactionVersion::V4) + } else if versions.contains(&5) { + Ok(TransactionVersion::V5) + } else { + Err(ExtrinsicError::UnsupportedVersion.into()) + } +} + +/// The transaction versions supported by Subxt. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum TransactionVersion { + /// v4 transactions (signed and unsigned transactions) + V4, + /// v5 transactions (bare and general transactions) + V5, +} + /// Return the SCALE encoded bytes representing the call data of the transaction. pub fn call_data(call: &Call, metadata: &Metadata) -> Result, Error> { let mut bytes = Vec::new(); @@ -96,10 +124,27 @@ pub fn call_data(call: &Call, metadata: &Metadata) -> Result( +/// Creates a V4 "unsigned" transaction without submitting it. +pub fn create_v4_unsigned( call: &Call, metadata: &Metadata, +) -> Result, Error> { + create_unsigned_at_version(call, 4, metadata) +} + +/// Creates a V5 "bare" transaction without submitting it. +pub fn create_v5_bare( + call: &Call, + metadata: &Metadata, +) -> Result, Error> { + create_unsigned_at_version(call, 5, metadata) +} + +// Create a V4 "unsigned" transaction or V5 "bare" transaction. +fn create_unsigned_at_version( + call: &Call, + tx_version: u8, + metadata: &Metadata, ) -> Result, Error> { // 1. Validate this call against the current node metadata if the call comes // with a hash allowing us to do so. @@ -108,8 +153,8 @@ pub fn create_unsigned( // 2. Encode extrinsic let extrinsic = { let mut encoded_inner = Vec::new(); - // transaction protocol version (4) (is not signed, so no 1 bit at the front). - 4u8.encode_to(&mut encoded_inner); + // encode the transaction version first. + tx_version.encode_to(&mut encoded_inner); // encode call data after this byte. call.encode_call_data_to(metadata, &mut encoded_inner)?; // now, prefix byte length: @@ -126,15 +171,12 @@ pub fn create_unsigned( Ok(Transaction::from_bytes(extrinsic)) } -/// Create a partial extrinsic. -/// -/// 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_signed( +/// Construct a v4 extrinsic, ready to be signed. +pub fn create_v4_signed( call: &Call, client_state: &ClientState, params: >::Params, -) -> Result, Error> { +) -> Result, Error> { // 1. Validate this call against the current node metadata if the call comes // with a hash allowing us to do so. validate(call, &client_state.metadata)?; @@ -147,81 +189,83 @@ pub fn create_partial_signed( >::new(client_state, params)?; // Return these details, ready to construct a signed extrinsic from. - Ok(PartialTransaction { + Ok(PartialTransactionV4 { call_data, additional_and_extra_params, }) } -/// Creates a signed extrinsic without submitting it. -/// -/// 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_signed( +/// Construct a v5 "general" extrinsic, ready to be signed or emitted as is. +pub fn create_v5_general( call: &Call, client_state: &ClientState, - signer: &Signer, params: >::Params, -) -> Result, Error> -where - T: Config, - Call: Payload, - Signer: SignerT, -{ +) -> Result, Error> { // 1. Validate this call against the current node metadata if the call comes // with a hash allowing us to do so. validate(call, &client_state.metadata)?; - // 2. Gather the "additional" and "extra" params along with the encoded call data, - // ready to be signed. - let partial_signed = create_partial_signed(call, client_state, params)?; + // 2. Work out which TX extension version to target based on metadata (unless we + // explicitly ask for a specific transaction version at a later step). + let tx_extensions_version = client_state + .metadata + .extrinsic() + .transaction_extensions_version(); - // 3. Sign and construct an extrinsic from these details. - Ok(partial_signed.sign(signer)) + // 3. SCALE encode call data to bytes (pallet u8, call u8, call params). + let call_data = call_data(call, &client_state.metadata)?; + + // 4. Construct our custom additional/extra params. + let additional_and_extra_params = + >::new(client_state, params)?; + + // Return these details, ready to construct a signed extrinsic from. + Ok(PartialTransactionV5 { + call_data, + additional_and_extra_params, + tx_extensions_version, + }) } -/// This represents a partially constructed transaction that needs signing before it is ready -/// to submit. Use [`PartialTransaction::signer_payload()`] to return the payload that needs signing, -/// [`PartialTransaction::sign()`] to sign the transaction using a [`SignerT`] impl, or -/// [`PartialTransaction::sign_with_address_and_signature()`] to apply an existing signature and address -/// to the transaction. -pub struct PartialTransaction { +/// A partially constructed V4 extrinsic, ready to be signed. +pub struct PartialTransactionV4 { call_data: Vec, additional_and_extra_params: T::ExtrinsicParams, } -impl PartialTransaction { - // Obtain bytes representing the signer payload and run call some function - // with them. This can avoid an allocation in some cases when compared to - // [`PartialExtrinsic::signer_payload()`]. - 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_extra_to(&mut bytes); - self.additional_and_extra_params - .encode_additional_to(&mut bytes); - if bytes.len() > 256 { - f(Cow::Borrowed(blake2_256(&bytes).as_ref())) - } else { - f(Cow::Owned(bytes)) - } - } - - /// Return the signer payload for this extrinsic. These are the bytes that must - /// be signed in order to produce a valid signature for the extrinsic. - pub fn signer_payload(&self) -> Vec { - self.with_signer_payload(|bytes| bytes.to_vec()) - } - +impl PartialTransactionV4 { /// Return the bytes representing the call data for this partially constructed /// extrinsic. pub fn call_data(&self) -> &[u8] { &self.call_data } - /// Convert this [`PartialTransaction`] into a [`Transaction`], ready to submit. + // 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); + + if bytes.len() > 256 { + f(Cow::Borrowed(&blake2_256(&bytes))) + } else { + f(Cow::Owned(bytes)) + } + } + + /// Return the V4 signer payload for this extrinsic. These are the bytes that must + /// be signed in order to produce a valid signature for the extrinsic. + pub fn signer_payload(&self) -> Vec { + self.with_signer_payload(|bytes| bytes.to_vec()) + } + + /// Convert this [`PartialTransactionV4`] into a V4 signed [`Transaction`], 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(&self, signer: &Signer) -> Transaction @@ -231,30 +275,28 @@ impl PartialTransaction { // Given our signer, we can sign the payload representing this extrinsic. let signature = self.with_signer_payload(|bytes| signer.sign(&bytes)); // Now, use the signature and "from" address to build the extrinsic. - self.sign_with_address_and_signature(&signer.address(), &signature) + self.sign_with_account_and_signature(signer.account_id(), &signature) } - /// Convert this [`PartialTransaction`] into a [`Transaction`], 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_address_and_signature( + /// Convert this [`PartialTransactionV4`] into a V4 signed [`Transaction`], ready to submit. + /// The provided `address` and `signature` will be used. + pub fn sign_with_account_and_signature( &self, - address: &T::Address, + account_id: T::AccountId, signature: &T::Signature, ) -> Transaction { - // Encode the extrinsic (into the format expected by protocol version 4) let extrinsic = { let mut encoded_inner = Vec::new(); // "is signed" + transaction protocol version (4) (0b10000000 + 4u8).encode_to(&mut encoded_inner); // from address for signature + let address: T::Address = account_id.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_extra_to(&mut encoded_inner); + .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: @@ -272,6 +314,97 @@ impl PartialTransaction { } } +/// A partially constructed V5 general extrinsic, ready to be signed or emitted as-is. +pub struct PartialTransactionV5 { + call_data: Vec, + additional_and_extra_params: T::ExtrinsicParams, + tx_extensions_version: u8, +} + +impl PartialTransactionV5 { + /// Return the bytes representing the call data for this partially constructed + /// extrinsic. + pub fn call_data(&self) -> &[u8] { + &self.call_data + } + + /// Return the V5 signer payload for this extrinsic. These are the bytes that must + /// be signed in order to produce a valid signature for the extrinsic. + pub fn signer_payload(&self) -> [u8; 32] { + 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); + + blake2_256(&bytes) + } + + /// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`]. + /// + /// This transaction has not been explicitly signed. Use [`Self::sign`] + /// or [`Self::sign_with_account_and_signature`] if you wish to provide a + /// signature (this is usually a necessary step). + pub fn to_transaction(&self) -> Transaction { + let extrinsic = { + let mut encoded_inner = Vec::new(); + // "is general" + transaction protocol version (5) + (0b01000000 + 5u8).encode_to(&mut encoded_inner); + // Encode versions for the transaction extensions + self.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 + }; + + // Return an extrinsic ready to be submitted. + Transaction::from_bytes(extrinsic) + } + + /// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`] with a signature. + /// + /// Signing the transaction injects the signature into the transaction extension data, which is why + /// this method borrows self mutably. Signing repeatedly will override the previous signature. + pub fn sign(&mut self, signer: &Signer) -> Transaction + where + Signer: SignerT, + { + // 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 [`PartialTransactionV5`] into a V5 "general" [`Transaction`] with a signature. + /// Prefer [`Self::sign`] if you have a [`SignerT`] instance to use. + /// + /// Signing the transaction injects the signature into the transaction extension data, which is why + /// this method borrows self mutably. Signing repeatedly will override the previous signature. + pub fn sign_with_account_and_signature( + &mut self, + account_id: &T::AccountId, + signature: &T::Signature, + ) -> Transaction { + // Inject the signature into the transaction extensions + // before constructing it. + self.additional_and_extra_params + .inject_signature(account_id, signature); + + self.to_transaction() + } +} + /// This represents a signed transaction that's ready to be submitted. /// Use [`Transaction::encoded()`] or [`Transaction::into_encoded()`] to /// get the bytes for it, or [`Transaction::hash()`] to get the hash. diff --git a/core/src/tx/signer.rs b/core/src/tx/signer.rs index 85f1b38bc8..82dca378d1 100644 --- a/core/src/tx/signer.rs +++ b/core/src/tx/signer.rs @@ -14,9 +14,6 @@ pub trait Signer { /// Return the "from" account ID. fn account_id(&self) -> T::AccountId; - /// Return the "from" address. - fn address(&self) -> T::Address; - /// Takes a signer payload for an extrinsic, and returns a signature based on it. /// /// Some signers may fail, for instance because the hardware on which the keys are located has diff --git a/examples/wasm-example/Cargo.lock b/examples/wasm-example/Cargo.lock index 2d3eaa5f61..010917d83d 100644 --- a/examples/wasm-example/Cargo.lock +++ b/examples/wasm-example/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1784,15 +1784,6 @@ dependencies = [ "thiserror 1.0.58", ] -[[package]] -name = "polkadot-sdk" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb819108697967452fa6d8d96ab4c0d48cbaa423b3156499dcb24f1cf95d6775" -dependencies = [ - "sp-crypto-hashing", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -2485,15 +2476,11 @@ dependencies = [ "async-trait", "derive-where", "either", - "finito", "frame-metadata", "futures", - "getrandom", "hex", - "impl-serde", "jsonrpsee", "parity-scale-codec", - "polkadot-sdk", "primitive-types", "scale-bits", "scale-decode", @@ -2502,10 +2489,12 @@ dependencies = [ "scale-value", "serde", "serde_json", + "sp-crypto-hashing", "subxt-core", "subxt-lightclient", "subxt-macro", "subxt-metadata", + "subxt-rpcs", "thiserror 2.0.11", "tokio", "tracing", @@ -2544,7 +2533,6 @@ dependencies = [ "impl-serde", "keccak-hash", "parity-scale-codec", - "polkadot-sdk", "primitive-types", "scale-bits", "scale-decode", @@ -2553,6 +2541,7 @@ dependencies = [ "scale-value", "serde", "serde_json", + "sp-crypto-hashing", "subxt-metadata", "thiserror 2.0.11", "tracing", @@ -2605,11 +2594,36 @@ dependencies = [ "frame-metadata", "hashbrown 0.14.5", "parity-scale-codec", - "polkadot-sdk", "scale-info", + "sp-crypto-hashing", "thiserror 2.0.11", ] +[[package]] +name = "subxt-rpcs" +version = "0.39.0" +dependencies = [ + "derive-where", + "finito", + "frame-metadata", + "futures", + "getrandom", + "hex", + "impl-serde", + "jsonrpsee", + "parity-scale-codec", + "primitive-types", + "serde", + "serde_json", + "subxt-core", + "subxt-lightclient", + "thiserror 2.0.11", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + [[package]] name = "subxt-utils-fetchmetadata" version = "0.39.0" @@ -2765,6 +2779,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -2821,6 +2849,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.7", + "rand", "static_assertions", ] diff --git a/examples/wasm-example/src/routes/signing.rs b/examples/wasm-example/src/routes/signing.rs index 1c51305467..478f4b1430 100644 --- a/examples/wasm-example/src/routes/signing.rs +++ b/examples/wasm-example/src/routes/signing.rs @@ -6,7 +6,7 @@ use subxt::{OnlineClient, PolkadotConfig}; use subxt::config::DefaultExtrinsicParamsBuilder; use subxt::ext::codec::{Decode, Encode}; use subxt::tx::Payload as _; -use subxt::tx::SubmittableExtrinsic; +use subxt::tx::SubmittableTransaction; use subxt::utils::{AccountId32, MultiSignature}; use crate::services::{extension_signature_for_extrinsic, get_accounts, polkadot, Account}; @@ -51,7 +51,7 @@ pub enum SigningStage { pub enum SubmittingStage { Initial { - signed_extrinsic: SubmittableExtrinsic>, + signed_extrinsic: SubmittableTransaction>, }, Submitting, Success { @@ -70,7 +70,7 @@ pub enum Message { SignWithAccount(usize), ReceivedSignature( MultiSignature, - SubmittableExtrinsic>, + SubmittableTransaction>, ), SubmitSigned, ExtrinsicFinalized { @@ -166,15 +166,15 @@ impl Component for SigningExamplesComponent { let params = DefaultExtrinsicParamsBuilder::new() .nonce(account_nonce) .build(); - let Ok(partial_signed) = - api.tx().create_partial_signed_offline(&remark_call, params) + let Ok(mut partial_signed) = + api.tx().create_partial_offline(&remark_call, params) else { - return Message::Error(anyhow!("PartialExtrinsic creation failed")); + return Message::Error(anyhow!("PartialTransaction creation failed")); }; // Apply the signature let signed_extrinsic = partial_signed - .sign_with_address_and_signature(&account_id.into(), &multi_signature); + .sign_with_account_and_signature(&account_id, &multi_signature); // check the TX validity (to debug in the js console if the extrinsic would work) let dry_res = signed_extrinsic.validate().await; @@ -394,7 +394,7 @@ impl Component for SigningExamplesComponent { } async fn submit_wait_finalized_and_get_extrinsic_success_event( - extrinsic: SubmittableExtrinsic>, + extrinsic: SubmittableTransaction>, ) -> Result { let events = extrinsic .submit_and_watch() diff --git a/examples/wasm-example/src/services.rs b/examples/wasm-example/src/services.rs index 7897b05d95..270a95f860 100644 --- a/examples/wasm-example/src/services.rs +++ b/examples/wasm-example/src/services.rs @@ -140,7 +140,7 @@ pub async fn extension_signature_for_extrinsic( let signed_extensions: Vec = api .metadata() .extrinsic() - .signed_extensions() + .transaction_extensions() .iter() .map(|e| e.identifier().to_string()) .collect(); diff --git a/metadata/src/from_into/v15.rs b/metadata/src/from_into/v15.rs index 54fb60cbce..bfec3c9bea 100644 --- a/metadata/src/from_into/v15.rs +++ b/metadata/src/from_into/v15.rs @@ -8,10 +8,11 @@ use crate::utils::variant_index::VariantIndex; use crate::{ utils::ordered_map::OrderedMap, ArcStr, ConstantMetadata, ExtrinsicMetadata, Metadata, OuterEnumsMetadata, PalletMetadataInner, RuntimeApiMetadataInner, RuntimeApiMethodMetadata, - RuntimeApiMethodParamMetadata, SignedExtensionMetadata, StorageEntryMetadata, - StorageEntryModifier, StorageEntryType, StorageHasher, StorageMetadata, + RuntimeApiMethodParamMetadata, StorageEntryMetadata, StorageEntryModifier, StorageEntryType, + StorageHasher, StorageMetadata, TransactionExtensionMetadata, }; use alloc::borrow::ToOwned; +use alloc::vec; use frame_metadata::v15; use hashbrown::HashMap; use scale_info::form::PortableForm; @@ -102,8 +103,8 @@ mod from_v15 { fn from_signed_extension_metadata( value: v15::SignedExtensionMetadata, - ) -> SignedExtensionMetadata { - SignedExtensionMetadata { + ) -> TransactionExtensionMetadata { + TransactionExtensionMetadata { identifier: value.identifier, extra_ty: value.ty.id, additional_ty: value.additional_signed.id, @@ -112,8 +113,8 @@ mod from_v15 { fn from_extrinsic_metadata(value: v15::ExtrinsicMetadata) -> ExtrinsicMetadata { ExtrinsicMetadata { - version: value.version, - signed_extensions: value + supported_versions: vec![value.version], + transaction_extensions: value .signed_extensions .into_iter() .map(from_signed_extension_metadata) @@ -122,6 +123,7 @@ mod from_v15 { call_ty: value.call_ty.id, signature_ty: value.signature_ty.id, extra_ty: value.extra_ty.id, + transaction_extensions_version: 0, } } @@ -330,9 +332,23 @@ mod into_v15 { fn from_extrinsic_metadata(e: ExtrinsicMetadata) -> v15::ExtrinsicMetadata { v15::ExtrinsicMetadata { - version: e.version, + // V16 and above metadata can have multiple supported extrinsic versions. We have to + // pick just one of these if converting back to V14/V15 metadata. + // + // - Picking the largest may mean that older tooling won't be compatible (it may only + // check/support older version). + // - Picking the smallest may mean that newer tooling won't work, or newer methods won't + // work. + // + // Either could make sense, but we keep the oldest to prioritize backward compat with + // older tooling. + version: *e + .supported_versions + .iter() + .min() + .expect("at least one extrinsic version expected"), signed_extensions: e - .signed_extensions + .transaction_extensions .into_iter() .map(from_signed_extension_metadata) .collect(), @@ -344,7 +360,7 @@ mod into_v15 { } fn from_signed_extension_metadata( - s: SignedExtensionMetadata, + s: TransactionExtensionMetadata, ) -> v15::SignedExtensionMetadata { v15::SignedExtensionMetadata { identifier: s.identifier, diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 8dbcbcdbab..346d503788 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -131,7 +131,7 @@ impl frame_decode::extrinsics::ExtrinsicTypeInfo for Metadata { Ok(ExtrinsicExtensionInfo { extension_ids: self .extrinsic() - .signed_extensions() + .transaction_extensions() .iter() .map(|f| ExtrinsicInfoArg { name: Cow::Borrowed(f.identifier()), @@ -601,10 +601,14 @@ pub struct ExtrinsicMetadata { signature_ty: u32, /// The type of the outermost Extra enum. extra_ty: u32, - /// Extrinsic version. - version: u8, + /// Which extrinsic versions are supported by this chain. + supported_versions: Vec, /// The signed extensions in the order they appear in the extrinsic. - signed_extensions: Vec, + transaction_extensions: Vec, + /// Version of the transaction extensions. + // TODO [jsdw]: V16 metadata groups transaction extensions by version. + // need to work out what to do once there is more than one version to deal with. + transaction_extensions_version: u8, } impl ExtrinsicMetadata { @@ -626,20 +630,25 @@ impl ExtrinsicMetadata { self.extra_ty } - /// Extrinsic version. - pub fn version(&self) -> u8 { - self.version + /// Which extrinsic versions are supported. + pub fn supported_versions(&self) -> &[u8] { + &self.supported_versions } /// The extra/additional information associated with the extrinsic. - pub fn signed_extensions(&self) -> &[SignedExtensionMetadata] { - &self.signed_extensions + pub fn transaction_extensions(&self) -> &[TransactionExtensionMetadata] { + &self.transaction_extensions + } + + /// Which version are these transaction extensions? + pub fn transaction_extensions_version(&self) -> u8 { + self.transaction_extensions_version } } /// Metadata for the signed extensions used by extrinsics. #[derive(Debug, Clone)] -pub struct SignedExtensionMetadata { +pub struct TransactionExtensionMetadata { /// The unique signed extension identifier, which may be different from the type name. identifier: String, /// The type of the signed extension, with the data to be included in the extrinsic. @@ -648,7 +657,7 @@ pub struct SignedExtensionMetadata { additional_ty: u32, } -impl SignedExtensionMetadata { +impl TransactionExtensionMetadata { /// The unique signed extension identifier, which may be different from the type name. pub fn identifier(&self) -> &str { &self.identifier diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index 438b74bfd8..d476a54acb 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -95,7 +95,7 @@ impl TypeSet { self.insert(ty); } - for signed in &extrinsic.signed_extensions { + for signed in &extrinsic.transaction_extensions { self.insert(signed.extra_ty); self.insert(signed.additional_ty); } @@ -294,7 +294,7 @@ fn iterate_metadata_types(metadata: &mut Metadata) -> impl Iterator 32 { + panic!("The metadata validation logic does not support more than 32 extrinsic versions."); + } + let supported_extrinsic_versions = { + let mut a = [0u8; 32]; + a[0..extrinsic.supported_versions.len()].copy_from_slice(&extrinsic.supported_versions); + a + }; + let mut bytes = concat_and_hash4( &address_hash, &signature_hash, &extra_hash, - &[extrinsic.version; 32], + &supported_extrinsic_versions, ); - for signed_extension in extrinsic.signed_extensions.iter() { + for signed_extension in extrinsic.transaction_extensions.iter() { bytes = concat_and_hash4( &bytes, &hash(signed_extension.identifier.as_bytes()), diff --git a/signer/src/ecdsa.rs b/signer/src/ecdsa.rs index 99a889101d..cd17342c77 100644 --- a/signer/src/ecdsa.rs +++ b/signer/src/ecdsa.rs @@ -345,10 +345,6 @@ mod subxt_compat { self.public_key().into() } - fn address(&self) -> T::Address { - self.public_key().into() - } - fn sign(&self, signer_payload: &[u8]) -> T::Signature { self.sign(signer_payload).into() } diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 8a4a91158b..ff7d51365e 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -164,7 +164,7 @@ impl FromStr for DerivationPath { } /// A signature generated by [`Keypair::sign()`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode, codec::Decode)] pub struct Signature(pub [u8; 65]); impl AsRef<[u8; 65]> for Signature { @@ -267,10 +267,6 @@ mod subxt_compat { self.public_key().into() } - fn address(&self) -> T::Address { - self.public_key().into() - } - fn sign(&self, signer_payload: &[u8]) -> T::Signature { self.sign(signer_payload).into() } @@ -404,11 +400,6 @@ mod test { } - #[test] - fn check_account_id_eq_address(keypair in keypair()) { - assert_eq!(SubxtSigner::account_id(&keypair), SubxtSigner::address(&keypair)); - } - #[test] fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") { let sig = SubxtSigner::sign(&keypair, msg.as_bytes()); diff --git a/signer/src/sr25519.rs b/signer/src/sr25519.rs index 187c335a9f..6990440386 100644 --- a/signer/src/sr25519.rs +++ b/signer/src/sr25519.rs @@ -338,10 +338,6 @@ mod subxt_compat { self.public_key().into() } - fn address(&self) -> T::Address { - self.public_key().into() - } - fn sign(&self, signer_payload: &[u8]) -> T::Signature { self.sign(signer_payload).into() } diff --git a/subxt/examples/block_decoding_dynamic.rs b/subxt/examples/block_decoding_dynamic.rs index 3826d189ce..aa439da10e 100644 --- a/subxt/examples/block_decoding_dynamic.rs +++ b/subxt/examples/block_decoding_dynamic.rs @@ -17,7 +17,7 @@ async fn main() -> Result<(), Box> { // Decode each signed extrinsic in the block dynamically let extrinsics = block.extrinsics().await?; for ext in extrinsics.iter() { - let Some(signed_extensions) = ext.signed_extensions() else { + let Some(transaction_extensions) = ext.transaction_extensions() else { continue; // we do not look at inherents in this example }; @@ -25,8 +25,8 @@ async fn main() -> Result<(), Box> { let fields = ext.field_values()?; println!(" {}/{}", meta.pallet.name(), meta.variant.name); - println!(" Signed Extensions:"); - for signed_ext in signed_extensions.iter() { + println!(" Transaction Extensions:"); + for signed_ext in transaction_extensions.iter() { // We only want to take a look at these 3 signed extensions, because the others all just have unit fields. if ["CheckMortality", "CheckNonce", "ChargeTransactionPayment"] .contains(&signed_ext.name()) diff --git a/subxt/examples/block_decoding_static.rs b/subxt/examples/block_decoding_static.rs index d997dc0e22..26ae80e1a4 100644 --- a/subxt/examples/block_decoding_static.rs +++ b/subxt/examples/block_decoding_static.rs @@ -30,7 +30,7 @@ async fn main() -> Result<(), Box> { for transfer in extrinsics.find::() { let transfer = transfer?; - let Some(extensions) = transfer.details.signed_extensions() else { + let Some(extensions) = transfer.details.transaction_extensions() else { panic!("TransferKeepAlive should be signed") }; diff --git a/subxt/examples/blocks_subscribing.rs b/subxt/examples/blocks_subscribing.rs index 4a6163d80f..8d50b97765 100644 --- a/subxt/examples/blocks_subscribing.rs +++ b/subxt/examples/blocks_subscribing.rs @@ -48,11 +48,11 @@ async fn main() -> Result<(), Box> { println!(" {}", event_values); } - println!(" Signed Extensions:"); - if let Some(signed_extensions) = ext.signed_extensions() { - for signed_extension in signed_extensions.iter() { - let name = signed_extension.name(); - let value = signed_extension.value()?.to_string(); + println!(" Transaction Extensions:"); + if let Some(transaction_extensions) = ext.transaction_extensions() { + for transaction_extension in transaction_extensions.iter() { + let name = transaction_extension.name(); + let value = transaction_extension.value()?.to_string(); println!(" {name}: {value}"); } } diff --git a/subxt/examples/rpc_legacy.rs b/subxt/examples/rpc_legacy.rs index 84bd837e2d..a21afd8f97 100644 --- a/subxt/examples/rpc_legacy.rs +++ b/subxt/examples/rpc_legacy.rs @@ -38,12 +38,8 @@ async fn main() -> Result<(), Box> { let current_nonce = rpc .system_account_next_index(&alice.public_key().into()) .await?; - let current_header = rpc.chain_get_header(None).await?.unwrap(); - let ext_params = Params::new() - .mortal(¤t_header, 8) - .nonce(current_nonce) - .build(); + let ext_params = Params::new().mortal(8).nonce(current_nonce).build(); let balance_transfer = polkadot::tx() .balances() @@ -51,7 +47,8 @@ async fn main() -> Result<(), Box> { let ext_hash = api .tx() - .create_signed_offline(&balance_transfer, &alice, ext_params)? + .create_partial_offline(&balance_transfer, ext_params)? + .sign(&alice) .submit() .await?; diff --git a/subxt/examples/setup_config_custom.rs b/subxt/examples/setup_config_custom.rs index cc4900d5d7..d146c406f5 100644 --- a/subxt/examples/setup_config_custom.rs +++ b/subxt/examples/setup_config_custom.rs @@ -2,7 +2,8 @@ use codec::Encode; use subxt::client::ClientState; use subxt::config::{ - Config, ExtrinsicParams, ExtrinsicParamsEncoder, ExtrinsicParamsError, RefineParams, + transaction_extensions::Params, Config, ExtrinsicParams, ExtrinsicParamsEncoder, + ExtrinsicParamsError, }; use subxt_signer::sr25519::dev; @@ -53,7 +54,7 @@ impl CustomExtrinsicParamsBuilder { } } -impl RefineParams for CustomExtrinsicParamsBuilder {} +impl Params for CustomExtrinsicParamsBuilder {} // Describe how to fetch and then encode the params: impl ExtrinsicParams for CustomExtrinsicParams { @@ -69,14 +70,12 @@ impl ExtrinsicParams for CustomExtrinsicParams { } } -impl RefineParams for CustomExtrinsicParams {} - // Encode the relevant params when asked: impl ExtrinsicParamsEncoder for CustomExtrinsicParams { - fn encode_extra_to(&self, v: &mut Vec) { + fn encode_value_to(&self, v: &mut Vec) { (self.tip, self.foo).encode_to(v); } - fn encode_additional_to(&self, v: &mut Vec) { + fn encode_implicit_to(&self, v: &mut Vec) { self.genesis_hash.encode_to(v) } } diff --git a/subxt/examples/setup_config_signed_extension.rs b/subxt/examples/setup_config_transaction_extension.rs similarity index 71% rename from subxt/examples/setup_config_signed_extension.rs rename to subxt/examples/setup_config_transaction_extension.rs index 0349d03380..9c3cfa3d41 100644 --- a/subxt/examples/setup_config_signed_extension.rs +++ b/subxt/examples/setup_config_transaction_extension.rs @@ -3,7 +3,7 @@ use codec::Encode; use scale_encode::EncodeAsType; use scale_info::PortableRegistry; use subxt::client::ClientState; -use subxt::config::signed_extensions; +use subxt::config::transaction_extensions; use subxt::config::{ Config, DefaultExtrinsicParamsBuilder, ExtrinsicParams, ExtrinsicParamsEncoder, ExtrinsicParamsError, @@ -25,53 +25,54 @@ impl Config for CustomConfig { type Signature = subxt::utils::MultiSignature; type Hasher = subxt::config::substrate::BlakeTwo256; type Header = subxt::config::substrate::SubstrateHeader; - type ExtrinsicParams = signed_extensions::AnyOf< + type ExtrinsicParams = transaction_extensions::AnyOf< Self, ( // Load in the existing signed extensions we're interested in // (if the extension isn't actually needed it'll just be ignored): - signed_extensions::CheckSpecVersion, - signed_extensions::CheckTxVersion, - signed_extensions::CheckNonce, - signed_extensions::CheckGenesis, - signed_extensions::CheckMortality, - signed_extensions::ChargeAssetTxPayment, - signed_extensions::ChargeTransactionPayment, - signed_extensions::CheckMetadataHash, + transaction_extensions::VerifySignature, + transaction_extensions::CheckSpecVersion, + transaction_extensions::CheckTxVersion, + transaction_extensions::CheckNonce, + transaction_extensions::CheckGenesis, + transaction_extensions::CheckMortality, + transaction_extensions::ChargeAssetTxPayment, + transaction_extensions::ChargeTransactionPayment, + transaction_extensions::CheckMetadataHash, // And add a new one of our own: - CustomSignedExtension, + CustomTransactionExtension, ), >; type AssetId = u32; } // Our custom signed extension doesn't do much: -pub struct CustomSignedExtension; +pub struct CustomTransactionExtension; // Give the extension a name; this allows `AnyOf` to look it // up in the chain metadata in order to know when and if to use it. -impl signed_extensions::SignedExtension for CustomSignedExtension { +impl transaction_extensions::TransactionExtension for CustomTransactionExtension { type Decoded = (); fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "CustomSignedExtension" + identifier == "CustomTransactionExtension" } } // Gather together any params we need for our signed extension, here none. -impl ExtrinsicParams for CustomSignedExtension { +impl ExtrinsicParams for CustomTransactionExtension { type Params = (); fn new(_client: &ClientState, _params: Self::Params) -> Result { - Ok(CustomSignedExtension) + Ok(CustomTransactionExtension) } } // Encode whatever the extension needs to provide when asked: -impl ExtrinsicParamsEncoder for CustomSignedExtension { - fn encode_extra_to(&self, v: &mut Vec) { +impl ExtrinsicParamsEncoder for CustomTransactionExtension { + fn encode_value_to(&self, v: &mut Vec) { "Hello".encode_to(v); } - fn encode_additional_to(&self, v: &mut Vec) { + fn encode_implicit_to(&self, v: &mut Vec) { true.encode_to(v) } } @@ -84,8 +85,8 @@ impl ExtrinsicParamsEncoder for CustomSignedExtension { pub fn custom( params: DefaultExtrinsicParamsBuilder, ) -> <::ExtrinsicParams as ExtrinsicParams>::Params { - let (a, b, c, d, e, f, g, h) = params.build(); - (a, b, c, d, e, f, g, h, ()) + let (a, b, c, d, e, f, g, h, i) = params.build(); + (a, b, c, d, e, f, g, h, i, ()) } #[tokio::main] diff --git a/subxt/examples/substrate_compat_signer.rs b/subxt/examples/substrate_compat_signer.rs index 92c472f394..3ef7662743 100644 --- a/subxt/examples/substrate_compat_signer.rs +++ b/subxt/examples/substrate_compat_signer.rs @@ -69,10 +69,6 @@ mod pair_signer { self.account_id.clone() } - fn address(&self) -> ::Address { - self.account_id.clone().into() - } - fn sign(&self, signer_payload: &[u8]) -> ::Signature { let signature = self.signer.sign(signer_payload); MultiSignature::Sr25519(signature.0) diff --git a/subxt/examples/tx_partial.rs b/subxt/examples/tx_partial.rs index c463a4e64f..0684091de6 100644 --- a/subxt/examples/tx_partial.rs +++ b/subxt/examples/tx_partial.rs @@ -10,7 +10,7 @@ pub mod polkadot {} #[tokio::main] async fn main() -> Result<(), BoxedError> { // Spawned tasks require things held across await points to impl Send, - // so we use one to demonstrate that this is possible with `PartialExtrinsic` + // so we use one to demonstrate that this is possible with `PartialTransaction` tokio::spawn(signing_example()).await??; Ok(()) } @@ -25,9 +25,9 @@ async fn signing_example() -> Result<(), BoxedError> { let alice = dev::alice(); // Create partial tx, ready to be signed. - let partial_tx = api + let mut partial_tx = api .tx() - .create_partial_signed( + .create_partial( &balance_transfer_tx, &alice.public_key().to_account_id(), Default::default(), @@ -35,13 +35,13 @@ async fn signing_example() -> Result<(), BoxedError> { .await?; // Simulate taking some time to get a signature back, in part to - // show that the `PartialExtrinsic` can be held across await points. + // show that the `PartialTransaction` can be held across await points. tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; let signature = alice.sign(&partial_tx.signer_payload()); // Sign the transaction. let tx = partial_tx - .sign_with_address_and_signature(&alice.public_key().to_address(), &signature.into()); + .sign_with_account_and_signature(&alice.public_key().to_account_id(), &signature.into()); // Submit it. tx.submit_and_watch() diff --git a/subxt/examples/tx_with_params.rs b/subxt/examples/tx_with_params.rs index cf893c36ca..00126a7f9f 100644 --- a/subxt/examples/tx_with_params.rs +++ b/subxt/examples/tx_with_params.rs @@ -15,14 +15,9 @@ async fn main() -> Result<(), Box> { let dest = dev::bob().public_key().into(); let tx = polkadot::tx().balances().transfer_allow_death(dest, 10_000); - let latest_block = api.blocks().at_latest().await?; - // Configure the transaction parameters; we give a small tip and set the // transaction to live for 32 blocks from the `latest_block` above. - let tx_params = Params::new() - .tip(1_000) - .mortal(latest_block.header(), 32) - .build(); + let tx_params = Params::new().tip(1_000).mortal(32).build(); // submit the transaction: let from = dev::alice(); diff --git a/subxt/src/blocks/extrinsic_types.rs b/subxt/src/blocks/extrinsic_types.rs index a7e28da556..a4d87e681d 100644 --- a/subxt/src/blocks/extrinsic_types.rs +++ b/subxt/src/blocks/extrinsic_types.rs @@ -16,7 +16,8 @@ use subxt_core::blocks::{ExtrinsicDetails as CoreExtrinsicDetails, Extrinsics as // Re-export anything that's directly returned/used in the APIs below. pub use subxt_core::blocks::{ - ExtrinsicMetadataDetails, ExtrinsicSignedExtension, ExtrinsicSignedExtensions, StaticExtrinsic, + ExtrinsicMetadataDetails, ExtrinsicTransactionExtension, ExtrinsicTransactionExtensions, + StaticExtrinsic, }; /// The body of a block. @@ -191,14 +192,14 @@ where self.inner.signature_bytes() } - /// See [`subxt_core::blocks::ExtrinsicDetails::signed_extensions_bytes()`]. - pub fn signed_extensions_bytes(&self) -> Option<&[u8]> { - self.inner.signed_extensions_bytes() + /// See [`subxt_core::blocks::ExtrinsicDetails::transaction_extensions_bytes()`]. + pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> { + self.inner.transaction_extensions_bytes() } - /// See [`subxt_core::blocks::ExtrinsicDetails::signed_extensions()`]. - pub fn signed_extensions(&self) -> Option> { - self.inner.signed_extensions() + /// See [`subxt_core::blocks::ExtrinsicDetails::transaction_extensions()`]. + pub fn transaction_extensions(&self) -> Option> { + self.inner.transaction_extensions() } /// See [`subxt_core::blocks::ExtrinsicDetails::pallet_index()`]. diff --git a/subxt/src/blocks/mod.rs b/subxt/src/blocks/mod.rs index 2b03a63102..a28b2a5919 100644 --- a/subxt/src/blocks/mod.rs +++ b/subxt/src/blocks/mod.rs @@ -14,8 +14,8 @@ pub use crate::backend::BlockRef; pub use block_types::Block; pub use blocks_client::BlocksClient; pub use extrinsic_types::{ - ExtrinsicDetails, ExtrinsicEvents, ExtrinsicSignedExtension, ExtrinsicSignedExtensions, - Extrinsics, FoundExtrinsic, StaticExtrinsic, + ExtrinsicDetails, ExtrinsicEvents, ExtrinsicTransactionExtension, + ExtrinsicTransactionExtensions, Extrinsics, FoundExtrinsic, StaticExtrinsic, }; // We get account nonce info in tx_client, too, so re-use the logic: diff --git a/subxt/src/book/setup/config.rs b/subxt/src/book/setup/config.rs index a26ee75a94..c2af70530a 100644 --- a/subxt/src/book/setup/config.rs +++ b/subxt/src/book/setup/config.rs @@ -64,10 +64,10 @@ //! //! ## ExtrinsicParams //! -//! Chains each have a set of "signed extensions" configured. Signed extensions provide a means to extend how transactions -//! work. Each signed extension can potentially encode some "extra" data which is sent along with a transaction, as well as some +//! Chains each have a set of "transaction extensions" (formally called "signed extensions") configured. Transaction extensions provide +//! a means to extend how transactions work. Each transaction extension can potentially encode some "extra" data which is sent along with a transaction, as well as some //! "additional" data which is included in the transaction signer payload, but not transmitted along with the transaction. On -//! a node, signed extensions can then perform additional checks on the submitted transactions to ensure their validity. +//! a node, transaction extensions can then perform additional checks on the submitted transactions to ensure their validity. //! //! The `ExtrinsicParams` config type expects to be given an implementation of the [`crate::config::ExtrinsicParams`] trait. //! Implementations of the [`crate::config::ExtrinsicParams`] trait are handed some parameters from Subxt itself, and can @@ -75,26 +75,26 @@ //! via the required [`crate::config::ExtrinsicParamsEncoder`] impl. //! //! **In most cases, the default [crate::config::DefaultExtrinsicParams] type will work**: it understands the "standard" -//! signed extensions that are in use, and allows the user to provide things like a tip, and set the extrinsic mortality via -//! [`crate::config::DefaultExtrinsicParamsBuilder`]. It will use the chain metadata to decide which signed extensions to use -//! and in which order. It will return an error if the chain uses a signed extension which it doesn't know how to handle. +//! transaction extensions that are in use, and allows the user to provide things like a tip, and set the extrinsic mortality via +//! [`crate::config::DefaultExtrinsicParamsBuilder`]. It will use the chain metadata to decide which transaction extensions to use +//! and in which order. It will return an error if the chain uses a transaction extension which it doesn't know how to handle. //! -//! If the chain uses novel signed extensions (or if you just wish to provide a different interface for users to configure +//! If the chain uses novel transaction extensions (or if you just wish to provide a different interface for users to configure //! transactions), you can either: //! -//! 1. Implement a new signed extension and add it to the list. +//! 1. Implement a new transaction extension and add it to the list. //! 2. Implement [`crate::config::DefaultExtrinsicParams`] from scratch. //! //! See below for examples of each. //! -//! ### Finding out which signed extensions a chain is using. +//! ### Finding out which transaction extensions a chain is using. //! -//! In either case, you'll want to find out which signed extensions a chain is using. This information can be obtained from -//! the `SignedExtra` parameter of the `UncheckedExtrinsic` of your parachain, which will be a tuple of signed extensions. +//! In either case, you'll want to find out which transaction extensions a chain is using. This information can be obtained from +//! the `SignedExtra` parameter of the `UncheckedExtrinsic` of your parachain, which will be a tuple of transaction extensions. //! It can also be obtained from the metadata (see [`frame_metadata::v15::SignedExtensionMetadata`]). //! -//! For statemint, the signed extensions look like -//! [this](https://github.com/paritytech/cumulus/tree/master/parachains/runtimes/assets/asset-hub-polkadot/src/lib.rs#L779): +//! For statemint, the transaction extensions look like +//! [this](https://github.com/paritytech/cumulus/blob/d4bb2215bb28ee05159c4c7df1b3435177b5bf4e/parachains/runtimes/assets/asset-hub-polkadot/src/lib.rs#L786): //! //! ```rs //! pub type SignedExtra = ( @@ -126,20 +126,20 @@ //! //! All types in the `struct type` column make up the "extra" data that we're expected to provide. All types in the //! `AdditionalSigned` column make up the "additional" data that we're expected to provide. This information will be useful -//! whether we want to implement [`crate::config::SignedExtension`] for a signed extension, or implement +//! whether we want to implement [`crate::config::TransactionExtension`] for a transaction extension, or implement //! [`crate::config::ExtrinsicParams`] from scratch. //! -//! As it happens, all of the signed extensions in the table are either already exported in [`crate::config::signed_extensions`], +//! As it happens, all of the transaction extensions in the table are either already exported in [`crate::config::transaction_extensions`], //! or they hand back no "additional" or "extra" data. In both of these cases, the default `ExtrinsicParams` configuration will //! work out of the box. //! -//! ### Implementing and adding new signed extensions to the config +//! ### Implementing and adding new transaction extensions to the config //! -//! If you do need to implement a novel signed extension, then you can implement [`crate::config::signed_extensions::SignedExtension`] -//! on a custom type and place it into a new set of signed extensions, like so: +//! If you do need to implement a novel transaction extension, then you can implement [`crate::config::transaction_extensions::TransactionExtension`] +//! on a custom type and place it into a new set of transaction extensions, like so: //! //! ```rust,ignore -#![doc = include_str ! ("../../../examples/setup_config_signed_extension.rs")] +#![doc = include_str ! ("../../../examples/setup_config_transaction_extension.rs")] //! ``` //! //! ### Implementing [`crate::config::ExtrinsicParams`] from scratch diff --git a/subxt/src/book/usage/blocks.rs b/subxt/src/book/usage/blocks.rs index a36b02f927..e6e714a649 100644 --- a/subxt/src/book/usage/blocks.rs +++ b/subxt/src/book/usage/blocks.rs @@ -49,7 +49,7 @@ //! //! This example shows how to subscribe to blocks and decode the extrinsics in each block into the root extrinsic type. //! Once we get hold of the [ExtrinsicDetails](crate::blocks::ExtrinsicDetails), we can decode it statically or dynamically. -//! We can also access details about the extrinsic, including the associated events and signed extensions. +//! We can also access details about the extrinsic, including the associated events and transaction extensions. //! //! ```rust,ignore #![doc = include_str!("../../../examples/blocks_subscribing.rs")] @@ -64,7 +64,8 @@ //! get only [the first one](crate::blocks::Extrinsics::find_first), or [the last one](crate::blocks::Extrinsics::find_last). //! //! The following example monitors `TransferKeepAlive` extrinsics on the Polkadot network. -//! We statically decode them and access the [tip](crate::blocks::ExtrinsicSignedExtensions::tip()) and [account nonce](crate::blocks::ExtrinsicSignedExtensions::nonce()) signed extensions. +//! We statically decode them and access the [tip](crate::blocks::ExtrinsicTransactionExtensions::tip()) and [account nonce](crate::blocks::ExtrinsicTransactionExtensions::nonce()) +//! transaction extensions. //! //! ```rust,ignore #![doc = include_str!("../../../examples/block_decoding_static.rs")] @@ -75,7 +76,7 @@ //! Sometimes you might use subxt with metadata that is not known at compile time. In this case, you do not have access to a statically generated //! interface module that contains the relevant Rust types. You can [decode ExtrinsicDetails dynamically](crate::blocks::ExtrinsicDetails::field_values()), //! which gives you access to it's fields as a [scale value composite](scale_value::Composite). -//! The following example looks for signed extrinsics on the Polkadot network and retrieves their pallet name, variant name, data fields and signed extensions dynamically. +//! The following example looks for signed extrinsics on the Polkadot network and retrieves their pallet name, variant name, data fields and transaction extensions dynamically. //! Notice how we do not need to use code generation via the subxt macro. The only fixed component we provide is the [PolkadotConfig](crate::config::PolkadotConfig). //! Other than that it works in a chain-agnostic way: //! @@ -83,16 +84,16 @@ #![doc = include_str!("../../../examples/block_decoding_dynamic.rs")] //! ``` //! -//! ## Decoding signed extensions +//! ## Decoding transaction extensions //! -//! Extrinsics can contain signed extensions. The signed extensions can be different across chains. -//! The [Config](crate::Config) implementation for your chain defines which signed extensions you expect. +//! Extrinsics can contain transaction extensions. The transaction extensions can be different across chains. +//! The [Config](crate::Config) implementation for your chain defines which transaction extensions you expect. //! Once you get hold of the [ExtrinsicDetails](crate::blocks::ExtrinsicDetails) for an extrinsic you are interested in, -//! you can try to [get its signed extensions](crate::blocks::ExtrinsicDetails::signed_extensions()). -//! These are only available on signed extrinsics. You can try to [find a specific signed extension](crate::blocks::ExtrinsicSignedExtensions::find), -//! in the returned [signed extensions](crate::blocks::ExtrinsicSignedExtensions). +//! you can try to [get its transaction extensions](crate::blocks::ExtrinsicDetails::transaction_extensions()). +//! These are only available on V4 signed extrinsics or V5 general extrinsics. You can try to [find a specific transaction extension](crate::blocks::ExtrinsicTransactionExtensions::find), +//! in the returned [transaction extensions](crate::blocks::ExtrinsicTransactionExtensions). //! -//! Subxt also provides utility functions to get the [tip](crate::blocks::ExtrinsicSignedExtensions::tip()) and the -//! [account nonce](crate::blocks::ExtrinsicSignedExtensions::tip()) associated with an extrinsic, given its signed extensions. -//! If you prefer to do things dynamically you can get the data of the signed extension as a [scale value](crate::blocks::ExtrinsicSignedExtension::value()). +//! Subxt also provides utility functions to get the [tip](crate::blocks::ExtrinsicTransactionExtensions::tip()) and the +//! [account nonce](crate::blocks::ExtrinsicTransactionExtensions::tip()) associated with an extrinsic, given its transaction extensions. +//! If you prefer to do things dynamically you can get the data of the transaction extension as a [scale value](crate::blocks::ExtrinsicTransactionExtension::value()). //! diff --git a/subxt/src/book/usage/transactions.rs b/subxt/src/book/usage/transactions.rs index 86d2f8ef8a..2cfe818682 100644 --- a/subxt/src/book/usage/transactions.rs +++ b/subxt/src/book/usage/transactions.rs @@ -113,8 +113,8 @@ //! ]); //! //! // Construct the tx but don't sign it. The account nonce here defaults to 0. -//! // You can use `create_partial_signed` to fetch the correct nonce. -//! let partial_tx = client.tx().create_partial_signed_offline( +//! // You can use `create_partial` to fetch the correct nonce. +//! let mut partial_tx = client.tx().create_partial_offline( //! &payload, //! Default::default() //! )?; @@ -126,16 +126,16 @@ //! // Ultimately we need to be given back a `signature` (or really, anything //! // that can be SCALE encoded) and an `address`: //! let signature; -//! let address; +//! let account_id; //! # use subxt::tx::Signer; //! # let signer = subxt_signer::sr25519::dev::alice(); //! # signature = signer.sign(&signer_payload).into(); -//! # address = signer.public_key().to_address(); +//! # account_id = signer.public_key().to_account_id(); //! //! // Now we can build an tx, which one can call `submit` or `submit_and_watch` //! // on to submit to a node and optionally watch the status. -//! let tx = partial_tx.sign_with_address_and_signature( -//! &address, +//! let tx = partial_tx.sign_with_account_and_signature( +//! &account_id, //! &signature //! ); //! # Ok(()) diff --git a/subxt/src/error/mod.rs b/subxt/src/error/mod.rs index 3fe1d09a63..282b055bd7 100644 --- a/subxt/src/error/mod.rs +++ b/subxt/src/error/mod.rs @@ -21,7 +21,7 @@ pub use dispatch_error::{ pub use crate::metadata::Metadata; pub use scale_decode::Error as DecodeError; pub use scale_encode::Error as EncodeError; -pub use subxt_core::error::{ExtrinsicParamsError, MetadataError, StorageAddressError}; +pub use subxt_core::error::{ExtrinsicError, MetadataError, StorageAddressError}; pub use subxt_metadata::TryFromError as MetadataTryFromError; /// The underlying error enum, generic over the type held by the `Runtime` @@ -62,7 +62,7 @@ pub enum Error { Transaction(#[from] TransactionError), /// Error constructing the appropriate extrinsic params. #[error("Extrinsic params error: {0}")] - ExtrinsicParams(#[from] ExtrinsicParamsError), + Extrinsic(#[from] ExtrinsicError), /// Block related error. #[error("Block error: {0}")] Block(#[from] BlockError), @@ -90,7 +90,7 @@ impl From for Error { CoreError::StorageAddress(e) => Error::StorageAddress(e), CoreError::Decode(e) => Error::Decode(e), CoreError::Encode(e) => Error::Encode(e), - CoreError::ExtrinsicParams(e) => Error::ExtrinsicParams(e), + CoreError::Extrinsic(e) => Error::Extrinsic(e), CoreError::Block(e) => Error::Block(e.into()), } } diff --git a/subxt/src/lib.rs b/subxt/src/lib.rs index 1dc11a78ac..0db67754ca 100644 --- a/subxt/src/lib.rs +++ b/subxt/src/lib.rs @@ -57,10 +57,10 @@ pub mod utils; /// Polkadot node. pub mod config { pub use subxt_core::config::{ - polkadot, signed_extensions, substrate, BlockHash, Config, DefaultExtrinsicParams, + polkadot, substrate, transaction_extensions, BlockHash, Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, ExtrinsicParams, ExtrinsicParamsEncoder, Hasher, Header, - PolkadotConfig, PolkadotExtrinsicParams, RefineParams, RefineParamsData, SignedExtension, - SubstrateConfig, SubstrateExtrinsicParams, + PolkadotConfig, PolkadotExtrinsicParams, SubstrateConfig, SubstrateExtrinsicParams, + TransactionExtension, }; pub use subxt_core::error::ExtrinsicParamsError; } diff --git a/subxt/src/tx/mod.rs b/subxt/src/tx/mod.rs index 9f8f527a53..a920e644c2 100644 --- a/subxt/src/tx/mod.rs +++ b/subxt/src/tx/mod.rs @@ -15,7 +15,7 @@ mod tx_progress; pub use subxt_core::tx::payload::{dynamic, DefaultPayload, DynamicPayload, Payload}; pub use subxt_core::tx::signer::{self, Signer}; pub use tx_client::{ - PartialExtrinsic, SubmittableExtrinsic, TransactionInvalid, TransactionUnknown, TxClient, + PartialTransaction, SubmittableTransaction, TransactionInvalid, TransactionUnknown, TxClient, ValidationResult, }; pub use tx_progress::{TxInBlock, TxProgress, TxStatus}; diff --git a/subxt/src/tx/tx_client.rs b/subxt/src/tx/tx_client.rs index 43c5ecfa18..41ddc0ae95 100644 --- a/subxt/src/tx/tx_client.rs +++ b/subxt/src/tx/tx_client.rs @@ -5,13 +5,14 @@ use crate::{ backend::{BackendExt, BlockRef, TransactionStatus}, client::{OfflineClientT, OnlineClientT}, - config::{Config, ExtrinsicParams, Header, RefineParams, RefineParamsData}, + config::{Config, ExtrinsicParams, Header}, error::{BlockError, Error}, tx::{Payload, Signer as SignerT, TxProgress}, utils::PhantomDataSendSync, }; use codec::{Compact, Decode, Encode}; use derive_where::derive_where; +use subxt_core::tx::TransactionVersion; /// A client for working with transactions. #[derive_where(Clone; Client)] @@ -31,7 +32,7 @@ impl TxClient { } impl> TxClient { - /// Run the validation logic against some extrinsic you'd like to submit. Returns `Ok(())` + /// 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). @@ -50,59 +51,143 @@ impl> TxClient { subxt_core::tx::call_data(call, &self.client.metadata()).map_err(Into::into) } - /// Creates an unsigned extrinsic without submitting it. - pub fn create_unsigned(&self, call: &Call) -> Result, Error> + /// 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. + pub fn create_unsigned(&self, call: &Call) -> Result, Error> where Call: Payload, { - subxt_core::tx::create_unsigned(call, &self.client.metadata()) - .map(|tx| SubmittableExtrinsic { - client: self.client.clone(), - inner: tx, - }) - .map_err(Into::into) + let metadata = self.client.metadata(); + let tx = match subxt_core::tx::suggested_version(&metadata)? { + TransactionVersion::V4 => subxt_core::tx::create_v4_unsigned(call, &metadata), + TransactionVersion::V5 => subxt_core::tx::create_v5_bare(call, &metadata), + }?; + + Ok(SubmittableTransaction { + client: self.client.clone(), + inner: tx, + }) } - /// Create a partial extrinsic. + /// Creates a v4 unsigned (no signature or transaction extensions) transaction without submitting it. + /// + /// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick the + /// most suitable one for the given chain. + pub fn create_v4_unsigned( + &self, + call: &Call, + ) -> Result, Error> + where + Call: Payload, + { + let metadata = self.client.metadata(); + let tx = subxt_core::tx::create_v4_unsigned(call, &metadata)?; + + Ok(SubmittableTransaction { + client: self.client.clone(), + inner: tx, + }) + } + + /// Creates a v5 "bare" (no signature or transaction extensions) transaction without submitting it. + /// + /// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick the + /// most suitable one for the given chain. + pub fn create_v5_bare(&self, call: &Call) -> Result, Error> + where + Call: Payload, + { + let metadata = self.client.metadata(); + let tx = subxt_core::tx::create_v5_bare(call, &metadata)?; + + Ok(SubmittableTransaction { + client: self.client.clone(), + inner: tx, + }) + } + + /// 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 /// /// 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_signed_offline( + pub fn create_partial_offline( &self, call: &Call, params: >::Params, - ) -> Result, Error> + ) -> Result, Error> where Call: Payload, { - subxt_core::tx::create_partial_signed(call, &self.client.client_state(), params) - .map(|tx| PartialExtrinsic { - client: self.client.clone(), - inner: tx, - }) - .map_err(Into::into) + let metadata = self.client.metadata(); + let tx = match subxt_core::tx::suggested_version(&metadata)? { + TransactionVersion::V4 => PartialTransactionInner::V4( + subxt_core::tx::create_v4_signed(call, &self.client.client_state(), params)?, + ), + TransactionVersion::V5 => PartialTransactionInner::V5( + subxt_core::tx::create_v5_general(call, &self.client.client_state(), params)?, + ), + }; + + Ok(PartialTransaction { + client: self.client.clone(), + inner: tx, + }) } - /// Creates a signed extrinsic without submitting it. + /// 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. - pub fn create_signed_offline( + /// + /// Prefer [`Self::create_partial_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( &self, call: &Call, - signer: &Signer, params: >::Params, - ) -> Result, Error> + ) -> Result, Error> where Call: Payload, - Signer: SignerT, { - subxt_core::tx::create_signed(call, &self.client.client_state(), signer, params) - .map(|tx| SubmittableExtrinsic { - client: self.client.clone(), - inner: tx, - }) - .map_err(Into::into) + let tx = PartialTransactionInner::V4(subxt_core::tx::create_v4_signed( + call, + &self.client.client_state(), + params, + )?); + + Ok(PartialTransaction { + client: self.client.clone(), + inner: tx, + }) + } + + /// 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_partial_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( + &self, + call: &Call, + params: >::Params, + ) -> Result, Error> + where + Call: Payload, + { + let tx = PartialTransactionInner::V5(subxt_core::tx::create_v5_general( + call, + &self.client.client_state(), + params, + )?); + + Ok(PartialTransaction { + client: self.client.clone(), + inner: tx, + }) } } @@ -111,84 +196,85 @@ where T: Config, C: OnlineClientT, { - /// Fetch the latest block header and account nonce from the backend and use them to refine [`ExtrinsicParams::Params`]. - async fn refine_params( - &self, - account_id: &T::AccountId, - params: &mut >::Params, - ) -> Result<(), Error> { - let block_ref = self.client.backend().latest_finalized_block_ref().await?; - let block_header = self - .client - .backend() - .block_header(block_ref.hash()) - .await? - .ok_or_else(|| Error::Block(BlockError::not_found(block_ref.hash())))?; - let account_nonce = - crate::blocks::get_account_nonce(&self.client, account_id, block_ref.hash()).await?; - - params.refine(&RefineParamsData::new( - account_nonce, - block_header.number().into(), - block_header.hash(), - )); - Ok(()) - } - /// Get the account nonce for a given account ID. pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result { let block_ref = self.client.backend().latest_finalized_block_ref().await?; crate::blocks::get_account_nonce(&self.client, account_id, block_ref.hash()).await } - /// Creates a partial signed extrinsic, without submitting it. - pub async fn create_partial_signed( + /// Creates a partial transaction, without submitting it. This can then be signed and submitted. + pub async fn create_partial( &self, call: &Call, account_id: &T::AccountId, mut params: >::Params, - ) -> Result, Error> + ) -> Result, Error> where Call: Payload, { - // Refine the params by adding account nonce and latest block information: - self.refine_params(account_id, &mut params).await?; - // Create the partial extrinsic with the refined params: - self.create_partial_signed_offline(call, params) + inject_account_nonce_and_block(&self.client, account_id, &mut params).await?; + self.create_partial_offline(call, params) } - /// Creates a signed extrinsic, without submitting it. - pub async fn create_signed( + /// Creates a partial V4 transaction, without submitting it. This can then be signed and submitted. + /// + /// Prefer [`Self::create_partial()`] 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_partial( &self, call: &Call, + account_id: &T::AccountId, + mut params: >::Params, + ) -> Result, Error> + where + Call: Payload, + { + inject_account_nonce_and_block(&self.client, account_id, &mut params).await?; + self.create_v4_partial_offline(call, params) + } + + /// Creates a partial V5 transaction, without submitting it. This can then be signed and submitted. + /// + /// Prefer [`Self::create_partial()`] 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_partial( + &self, + call: &Call, + account_id: &T::AccountId, + mut params: >::Params, + ) -> Result, Error> + where + Call: Payload, + { + inject_account_nonce_and_block(&self.client, account_id, &mut params).await?; + self.create_v5_partial_offline(call, params) + } + + /// Creates a signed transaction, without submitting it. + pub async fn create_signed( + &mut self, + call: &Call, signer: &Signer, params: >::Params, - ) -> Result, Error> + ) -> Result, Error> where Call: Payload, Signer: SignerT, { - // 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. Gather the "additional" and "extra" params along with the encoded call data, - // ready to be signed. - let partial_signed = self - .create_partial_signed(call, &signer.account_id(), params) + let mut partial = self + .create_partial(call, &signer.account_id(), params) .await?; - // 3. Sign and construct an extrinsic from these details. - Ok(partial_signed.sign(signer)) + Ok(partial.sign(signer)) } - /// Creates and signs an extrinsic and submits it to the chain. Passes default parameters - /// to construct the "signed extra" and "additional" payloads needed by the extrinsic. + /// 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 [`TxProgress`], which can be used to track the status of the transaction /// and obtain details about it, once it has made it into a block. pub async fn sign_and_submit_then_watch_default( - &self, + &mut self, call: &Call, signer: &Signer, ) -> Result, Error> @@ -201,12 +287,12 @@ where .await } - /// Creates and signs an extrinsic and submits it to the chain. + /// Creates and signs an transaction and submits it to the chain. /// /// Returns a [`TxProgress`], which can be used to track the status of the transaction /// and obtain details about it, once it has made it into a block. pub async fn sign_and_submit_then_watch( - &self, + &mut self, call: &Call, signer: &Signer, params: >::Params, @@ -221,18 +307,18 @@ where .await } - /// Creates and signs an extrinsic and submits to the chain for block inclusion. Passes + /// 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 extrinsic. + /// by the transaction. /// - /// Returns `Ok` with the extrinsic hash if it is valid extrinsic. + /// Returns `Ok` with the transaction hash if it is valid transaction. /// /// # Note /// - /// Success does not mean the extrinsic has been included in the block, just that it is valid + /// 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( - &self, + &mut self, call: &Call, signer: &Signer, ) -> Result @@ -244,16 +330,16 @@ where self.sign_and_submit(call, signer, Default::default()).await } - /// Creates and signs an extrinsic and submits to the chain for block inclusion. + /// Creates and signs an transaction and submits to the chain for block inclusion. /// - /// Returns `Ok` with the extrinsic hash if it is valid extrinsic. + /// Returns `Ok` with the transaction hash if it is valid transaction. /// /// # Note /// - /// Success does not mean the extrinsic has been included in the block, just that it is valid + /// 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( - &self, + &mut self, call: &Call, signer: &Signer, params: >::Params, @@ -269,76 +355,99 @@ where } } -/// This payload contains the information needed to produce an extrinsic. -pub struct PartialExtrinsic { +/// This payload contains the information needed to produce an transaction. +pub struct PartialTransaction { client: C, - inner: subxt_core::tx::PartialTransaction, + inner: PartialTransactionInner, } -impl PartialExtrinsic +enum PartialTransactionInner { + V4(subxt_core::tx::PartialTransactionV4), + V5(subxt_core::tx::PartialTransactionV5), +} + +impl PartialTransaction where T: Config, C: OfflineClientT, { - /// Return the signer payload for this extrinsic. These are the bytes that must - /// be signed in order to produce a valid signature for the extrinsic. + /// 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.inner.signer_payload() - } - - /// Return the bytes representing the call data for this partially constructed - /// extrinsic. - pub fn call_data(&self) -> &[u8] { - self.inner.call_data() - } - - /// Convert this [`PartialExtrinsic`] into a [`SubmittableExtrinsic`], 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(&self, signer: &Signer) -> SubmittableExtrinsic - where - Signer: SignerT, - { - SubmittableExtrinsic { - client: self.client.clone(), - inner: self.inner.sign(signer), + match &self.inner { + PartialTransactionInner::V4(tx) => tx.signer_payload(), + PartialTransactionInner::V5(tx) => tx.signer_payload().to_vec(), } } - /// Convert this [`PartialExtrinsic`] into a [`SubmittableExtrinsic`], ready to submit. + /// Return the bytes representing the call data for this partially constructed + /// transaction. + pub fn call_data(&self) -> &[u8] { + match &self.inner { + PartialTransactionInner::V4(tx) => tx.call_data(), + PartialTransactionInner::V5(tx) => tx.call_data(), + } + } + + /// Convert this [`PartialTransaction`] 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: &Signer) -> SubmittableTransaction + where + Signer: SignerT, + { + let tx = match &mut self.inner { + PartialTransactionInner::V4(tx) => tx.sign(signer), + PartialTransactionInner::V5(tx) => tx.sign(signer), + }; + + SubmittableTransaction { + client: self.client.clone(), + inner: tx, + } + } + + /// 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 - /// [`PartialExtrinsic::sign()`] instead. - pub fn sign_with_address_and_signature( - &self, - address: &T::Address, + /// [`PartialTransaction::sign()`] instead. + pub fn sign_with_account_and_signature( + &mut self, + account_id: &T::AccountId, signature: &T::Signature, - ) -> SubmittableExtrinsic { - SubmittableExtrinsic { + ) -> SubmittableTransaction { + let tx = match &mut self.inner { + PartialTransactionInner::V4(tx) => { + tx.sign_with_account_and_signature(account_id.clone(), signature) + } + PartialTransactionInner::V5(tx) => { + tx.sign_with_account_and_signature(account_id, signature) + } + }; + + SubmittableTransaction { client: self.client.clone(), - inner: self - .inner - .sign_with_address_and_signature(address, signature), + inner: tx, } } } -/// This represents an extrinsic that has been signed and is ready to submit. -pub struct SubmittableExtrinsic { +/// This represents an transaction that has been signed and is ready to submit. +pub struct SubmittableTransaction { client: C, inner: subxt_core::tx::Transaction, } -impl SubmittableExtrinsic +impl SubmittableTransaction where T: Config, C: OfflineClientT, { - /// Create a [`SubmittableExtrinsic`] from some already-signed and prepared - /// extrinsic bytes, and some client (anything implementing [`OfflineClientT`] + /// Create a [`SubmittableTransaction`] from some already-signed and prepared + /// transaction bytes, and some client (anything implementing [`OfflineClientT`] /// or [`OnlineClientT`]). /// - /// Prefer to use [`TxClient`] to create and sign extrinsics. This is simply + /// Prefer to use [`TxClient`] to create and sign transactions. This is simply /// exposed in case you want to skip this process and submit something you've /// already created. pub fn from_bytes(client: C, tx_bytes: Vec) -> Self { @@ -348,34 +457,34 @@ where } } - /// Calculate and return the hash of the extrinsic, based on the configured hasher. + /// Calculate and return the hash of the transaction, based on the configured hasher. pub fn hash(&self) -> T::Hash { self.inner.hash() } - /// Returns the SCALE encoded extrinsic bytes. + /// Returns the SCALE encoded transaction bytes. pub fn encoded(&self) -> &[u8] { self.inner.encoded() } - /// Consumes [`SubmittableExtrinsic`] and returns the SCALE encoded - /// extrinsic bytes. + /// Consumes [`SubmittableTransaction`] and returns the SCALE encoded + /// transaction bytes. pub fn into_encoded(self) -> Vec { self.inner.into_encoded() } } -impl SubmittableExtrinsic +impl SubmittableTransaction where T: Config, C: OnlineClientT, { - /// Submits the extrinsic to the chain. + /// Submits the transaction to the chain. /// /// Returns a [`TxProgress`], which can be used to track the status of the transaction /// and obtain details about it, once it has made it into a block. pub async fn submit_and_watch(&self) -> Result, Error> { - // Get a hash of the extrinsic (we'll need this later). + // Get a hash of the transaction (we'll need this later). let ext_hash = self.hash(); // Submit and watch for transaction progress. @@ -388,7 +497,7 @@ where Ok(TxProgress::new(sub, self.client.clone(), ext_hash)) } - /// Submits the extrinsic to the chain for block inclusion. + /// 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 @@ -429,7 +538,7 @@ where /// 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 extrinsic. + /// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the transaction. pub async fn validate(&self) -> Result { let latest_block_ref = self.client.backend().latest_finalized_block_ref().await?; self.validate_at(latest_block_ref).await @@ -438,7 +547,7 @@ where /// 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 extrinsic. + /// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the transaction. pub async fn validate_at( &self, at: impl Into>, @@ -464,7 +573,7 @@ where ValidationResult::try_from_bytes(res) } - /// This returns an estimate for what the extrinsic is expected to cost to execute, less any tips. + /// 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(); @@ -486,6 +595,29 @@ where } } +/// 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>( + client: &Client, + account_id: &T::AccountId, + params: &mut >::Params, +) -> Result<(), Error> { + use subxt_core::config::transaction_extensions::Params; + + let block_ref = client.backend().latest_finalized_block_ref().await?; + let block_header = client + .backend() + .block_header(block_ref.hash()) + .await? + .ok_or_else(|| Error::Block(BlockError::not_found(block_ref.hash())))?; + let account_nonce = + crate::blocks::get_account_nonce(client, account_id, block_ref.hash()).await?; + + params.inject_account_nonce(account_nonce); + params.inject_block(block_header.number().into(), block_header.hash()); + + Ok(()) +} + impl ValidationResult { #[allow(clippy::get_first)] fn try_from_bytes(bytes: Vec) -> Result { @@ -512,7 +644,7 @@ impl ValidationResult { } } -/// The result of performing [`SubmittableExtrinsic::validate()`]. +/// The result of performing [`SubmittableTransaction::validate()`]. #[derive(Clone, Debug, PartialEq)] pub enum ValidationResult { /// The transaction is valid @@ -611,12 +743,12 @@ pub enum TransactionInvalid { ExhaustsResources, /// Any other custom invalid validity that is not covered by this enum. Custom(u8), - /// An extrinsic with a Mandatory dispatch resulted in Error. This is indicative of either a + /// 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 extrinsic with a mandatory dispatch tried to be validated. - /// This is invalid; only inherent extrinsics are allowed to have mandatory dispatches. + /// 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, diff --git a/testing/integration-tests/Cargo.toml b/testing/integration-tests/Cargo.toml index 08791458bc..767b758e8b 100644 --- a/testing/integration-tests/Cargo.toml +++ b/testing/integration-tests/Cargo.toml @@ -28,6 +28,7 @@ chainhead-backend = [] [dev-dependencies] assert_matches = { workspace = true } codec = { package = "parity-scale-codec", workspace = true, features = ["derive", "bit-vec"] } +frame-decode = { workspace = true } frame-metadata = { workspace = true } futures = { workspace = true } hex = { workspace = true } diff --git a/testing/integration-tests/src/full_client/blocks/mod.rs b/testing/integration-tests/src/full_client/blocks.rs similarity index 93% rename from testing/integration-tests/src/full_client/blocks/mod.rs rename to testing/integration-tests/src/full_client/blocks.rs index 5cc08153c4..5e74e7e858 100644 --- a/testing/integration-tests/src/full_client/blocks/mod.rs +++ b/testing/integration-tests/src/full_client/blocks.rs @@ -12,7 +12,7 @@ use crate::utils::node_runtime; #[cfg(fullclient)] use subxt::{ config::{ - signed_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce}, + transaction_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce}, DefaultExtrinsicParamsBuilder, SubstrateConfig, }, utils::Era, @@ -262,7 +262,7 @@ async fn fetch_block_and_decode_extrinsic_details() { #[cfg(fullclient)] #[subxt_test] -async fn decode_signed_extensions_from_blocks() { +async fn decode_transaction_extensions_from_blocks() { let ctx = test_context().await; let api = ctx.client(); let alice = dev::alice(); @@ -301,7 +301,7 @@ async fn decode_signed_extensions_from_blocks() { } let transaction1 = submit_transfer_extrinsic_and_get_it_back!(1234); - let extensions1 = transaction1.signed_extensions().unwrap(); + let extensions1 = transaction1.transaction_extensions().unwrap(); let nonce1 = extensions1.nonce().unwrap(); let nonce1_static = extensions1.find::().unwrap().unwrap(); @@ -313,7 +313,7 @@ async fn decode_signed_extensions_from_blocks() { .tip(); let transaction2 = submit_transfer_extrinsic_and_get_it_back!(5678); - let extensions2 = transaction2.signed_extensions().unwrap(); + let extensions2 = transaction2.transaction_extensions().unwrap(); let nonce2 = extensions2.nonce().unwrap(); let nonce2_static = extensions2.find::().unwrap().unwrap(); let tip2 = extensions2.tip().unwrap(); @@ -332,7 +332,7 @@ async fn decode_signed_extensions_from_blocks() { assert_eq!(tip2, 5678); assert_eq!(tip2, tip2_static); - let expected_signed_extensions = [ + let expected_transaction_extensions = [ "CheckNonZeroSender", "CheckSpecVersion", "CheckTxVersion", @@ -345,13 +345,25 @@ async fn decode_signed_extensions_from_blocks() { "WeightReclaim", ]; - assert_eq!(extensions1.iter().count(), expected_signed_extensions.len()); - for (e, expected_name) in extensions1.iter().zip(expected_signed_extensions.iter()) { + assert_eq!( + extensions1.iter().count(), + expected_transaction_extensions.len() + ); + for (e, expected_name) in extensions1 + .iter() + .zip(expected_transaction_extensions.iter()) + { assert_eq!(e.name(), *expected_name); } - assert_eq!(extensions2.iter().count(), expected_signed_extensions.len()); - for (e, expected_name) in extensions2.iter().zip(expected_signed_extensions.iter()) { + assert_eq!( + extensions2.iter().count(), + expected_transaction_extensions.len() + ); + for (e, expected_name) in extensions2 + .iter() + .zip(expected_transaction_extensions.iter()) + { assert_eq!(e.name(), *expected_name); } diff --git a/testing/integration-tests/src/full_client/client/chain_head_rpcs.rs b/testing/integration-tests/src/full_client/client/chain_head_rpcs.rs index 345c0fda4f..c938e87dd7 100644 --- a/testing/integration-tests/src/full_client/client/chain_head_rpcs.rs +++ b/testing/integration-tests/src/full_client/client/chain_head_rpcs.rs @@ -271,8 +271,9 @@ async fn transactionwatch_v1_submit_and_watch() { let tx_bytes = ctx .client() .tx() - .create_signed_offline(&payload, &dev::alice(), Default::default()) + .create_partial_offline(&payload, Default::default()) .unwrap() + .sign(&dev::alice()) .into_encoded(); // Test submitting it: @@ -337,8 +338,9 @@ async fn transaction_v1_broadcast() { let tx = ctx .client() .tx() - .create_signed_offline(&tx_payload, &dev::alice(), Default::default()) - .unwrap(); + .create_partial_offline(&tx_payload, Default::default()) + .unwrap() + .sign(&dev::alice()); let tx_hash = tx.hash(); let tx_bytes = tx.into_encoded(); @@ -407,8 +409,9 @@ async fn transaction_v1_stop() { let tx_bytes = ctx .client() .tx() - .create_signed_offline(&tx, &dev::alice(), Default::default()) + .create_partial_offline(&tx, Default::default()) .unwrap() + .sign(&dev::alice()) .into_encoded(); // Submit the transaction. diff --git a/testing/integration-tests/src/full_client/client/mod.rs b/testing/integration-tests/src/full_client/client/mod.rs index 197f6086c6..a943d3c360 100644 --- a/testing/integration-tests/src/full_client/client/mod.rs +++ b/testing/integration-tests/src/full_client/client/mod.rs @@ -190,9 +190,9 @@ async fn external_signing() { // Create a partial extrinsic. We can get the signer payload at this point, to be // signed externally. let tx = node_runtime::tx().preimage().note_preimage(vec![0u8]); - let partial_extrinsic = api + let mut partial_extrinsic = api .tx() - .create_partial_signed(&tx, &alice.public_key().into(), Default::default()) + .create_partial(&tx, &alice.public_key().into(), Default::default()) .await .unwrap(); @@ -202,7 +202,7 @@ async fn external_signing() { let signature = alice.sign(&signer_payload); // Use this to build a signed extrinsic. let extrinsic = partial_extrinsic - .sign_with_address_and_signature(&alice.public_key().into(), &signature.into()); + .sign_with_account_and_signature(&alice.public_key().into(), &signature.into()); // And now submit it. extrinsic diff --git a/testing/integration-tests/src/full_client/metadata/mod.rs b/testing/integration-tests/src/full_client/metadata/mod.rs deleted file mode 100644 index c8ef931d15..0000000000 --- a/testing/integration-tests/src/full_client/metadata/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2019-2025 Parity Technologies (UK) Ltd. -// This file is dual-licensed as Apache-2.0 or GPL-3.0. -// see LICENSE for license details. - -mod validation; diff --git a/testing/integration-tests/src/full_client/metadata/validation.rs b/testing/integration-tests/src/full_client/metadata_validation.rs similarity index 100% rename from testing/integration-tests/src/full_client/metadata/validation.rs rename to testing/integration-tests/src/full_client/metadata_validation.rs diff --git a/testing/integration-tests/src/full_client/mod.rs b/testing/integration-tests/src/full_client/mod.rs index 3750988830..8e4646625d 100644 --- a/testing/integration-tests/src/full_client/mod.rs +++ b/testing/integration-tests/src/full_client/mod.rs @@ -6,6 +6,7 @@ mod blocks; mod client; mod codegen; mod frame; -mod metadata; +mod metadata_validation; mod runtime_api; mod storage; +mod transactions; diff --git a/testing/integration-tests/src/full_client/runtime_api/mod.rs b/testing/integration-tests/src/full_client/runtime_api.rs similarity index 100% rename from testing/integration-tests/src/full_client/runtime_api/mod.rs rename to testing/integration-tests/src/full_client/runtime_api.rs diff --git a/testing/integration-tests/src/full_client/storage/mod.rs b/testing/integration-tests/src/full_client/storage.rs similarity index 100% rename from testing/integration-tests/src/full_client/storage/mod.rs rename to testing/integration-tests/src/full_client/storage.rs diff --git a/testing/integration-tests/src/full_client/transactions.rs b/testing/integration-tests/src/full_client/transactions.rs new file mode 100644 index 0000000000..93e6f0acc2 --- /dev/null +++ b/testing/integration-tests/src/full_client/transactions.rs @@ -0,0 +1,149 @@ +// Copyright 2019-2025 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +use crate::utils::node_runtime; +use crate::{subxt_test, test_context}; +use core::ops::Deref; +use frame_decode::extrinsics::ExtrinsicType; +use subxt_signer::sr25519::dev; + +// TODO: When VerifySignature exists on the substrate kitchensink runtime, +// let's try actuallty submitting v4 and v5 signed extrinsics to verify that +// they are actually accepted by the node. + +#[subxt_test] +async fn v4_unsigned_encode_decode() -> Result<(), subxt::Error> { + let ctx = test_context().await; + let api = ctx.client(); + let md = api.metadata(); + + let call = node_runtime::tx() + .balances() + .transfer_allow_death(dev::bob().public_key().into(), 1000); + + let tx_bytes = api.tx().create_v4_unsigned(&call).unwrap().into_encoded(); + let tx_bytes_cursor = &mut &*tx_bytes; + + let decoded = frame_decode::extrinsics::decode_extrinsic( + tx_bytes_cursor, + md.deref(), + api.metadata().types(), + ) + .unwrap(); + + assert_eq!(tx_bytes_cursor.len(), 0); + assert_eq!(decoded.version(), 4); + assert_eq!(decoded.ty(), ExtrinsicType::Bare); + assert_eq!(decoded.pallet_name(), "Balances"); + assert_eq!(decoded.call_name(), "transfer_allow_death"); + assert!(decoded.signature_payload().is_none()); + + Ok(()) +} + +#[subxt_test] +async fn v5_bare_encode_decode() -> Result<(), subxt::Error> { + let ctx = test_context().await; + let api = ctx.client(); + let md = api.metadata(); + + let call = node_runtime::tx() + .balances() + .transfer_allow_death(dev::bob().public_key().into(), 1000); + + let tx_bytes = api.tx().create_v5_bare(&call).unwrap().into_encoded(); + let tx_bytes_cursor = &mut &*tx_bytes; + + let decoded = frame_decode::extrinsics::decode_extrinsic( + tx_bytes_cursor, + md.deref(), + api.metadata().types(), + ) + .unwrap(); + + assert_eq!(tx_bytes_cursor.len(), 0); + assert_eq!(decoded.version(), 5); + assert_eq!(decoded.ty(), ExtrinsicType::Bare); + assert_eq!(decoded.pallet_name(), "Balances"); + assert_eq!(decoded.call_name(), "transfer_allow_death"); + assert!(decoded.transaction_extension_payload().is_none()); + assert!(decoded.signature_payload().is_none()); + + Ok(()) +} + +#[subxt_test] +async fn v4_signed_encode_decode() -> Result<(), subxt::Error> { + let ctx = test_context().await; + let api = ctx.client(); + let md = api.metadata(); + + let call = node_runtime::tx() + .balances() + .transfer_allow_death(dev::bob().public_key().into(), 1000); + + let tx_bytes = api + .tx() + .create_v4_partial(&call, &dev::alice().public_key().into(), Default::default()) + .await + .unwrap() + .sign(&dev::alice()) + .into_encoded(); + let tx_bytes_cursor = &mut &*tx_bytes; + + let decoded = frame_decode::extrinsics::decode_extrinsic( + tx_bytes_cursor, + md.deref(), + api.metadata().types(), + ) + .unwrap(); + + assert_eq!(tx_bytes_cursor.len(), 0); + assert_eq!(decoded.version(), 4); + assert_eq!(decoded.ty(), ExtrinsicType::Signed); + assert_eq!(decoded.pallet_name(), "Balances"); + assert_eq!(decoded.call_name(), "transfer_allow_death"); + assert!(decoded.signature_payload().is_some()); + + Ok(()) +} + +#[subxt_test] +async fn v5_general_encode_decode() -> Result<(), subxt::Error> { + let ctx = test_context().await; + let api = ctx.client(); + let md = api.metadata(); + let dummy_signer = dev::alice(); + + let call = node_runtime::tx() + .balances() + .transfer_allow_death(dev::bob().public_key().into(), 1000); + + let tx_bytes = api + .tx() + .create_v5_partial(&call, &dev::alice().public_key().into(), Default::default()) + .await + .unwrap() + .sign(&dummy_signer) // No signature payload is added, but may be inserted into tx extensions. + .into_encoded(); + let tx_bytes_cursor = &mut &*tx_bytes; + + let decoded = frame_decode::extrinsics::decode_extrinsic( + tx_bytes_cursor, + md.deref(), + api.metadata().types(), + ) + .unwrap(); + + assert_eq!(tx_bytes_cursor.len(), 0); + assert_eq!(decoded.version(), 5); + assert_eq!(decoded.ty(), ExtrinsicType::General); + assert_eq!(decoded.pallet_name(), "Balances"); + assert_eq!(decoded.call_name(), "transfer_allow_death"); + assert!(decoded.transaction_extension_payload().is_some()); + // v5 general extrinsics have no signature payload; signature in tx extensions: + assert!(decoded.signature_payload().is_none()); + + Ok(()) +}