diff --git a/subxt/examples/setup_config_custom.rs b/subxt/examples/setup_config_custom.rs index edc21fcb11..6768787600 100644 --- a/subxt/examples/setup_config_custom.rs +++ b/subxt/examples/setup_config_custom.rs @@ -3,7 +3,19 @@ use subxt::client::OfflineClientT; use subxt::config::{Config, ExtrinsicParams, ExtrinsicParamsEncoder}; use subxt_signer::sr25519::dev; -#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale")] +#[subxt::subxt( + runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale", + derive_for_type(path = "xcm::v2::multilocation::MultiLocation", derive = "Clone"), + derive_for_type(path = "xcm::v2::multilocation::Junctions", derive = "Clone"), + derive_for_type(path = "xcm::v2::junction::Junction", derive = "Clone"), + derive_for_type(path = "xcm::v2::NetworkId", derive = "Clone"), + derive_for_type(path = "xcm::v2::BodyId", derive = "Clone"), + derive_for_type(path = "xcm::v2::BodyPart", derive = "Clone"), + derive_for_type( + path = "bounded_collections::weak_bounded_vec::WeakBoundedVec", + derive = "Clone" + ) +)] pub mod runtime {} use runtime::runtime_types::xcm::v2::multilocation::MultiLocation; diff --git a/subxt/examples/setup_config_signed_extension.rs b/subxt/examples/setup_config_signed_extension.rs index c74e5f655b..ab985a8838 100644 --- a/subxt/examples/setup_config_signed_extension.rs +++ b/subxt/examples/setup_config_signed_extension.rs @@ -1,4 +1,5 @@ use codec::Encode; +use scale_encode::EncodeAsType; use subxt::client::OfflineClientT; use subxt::config::signed_extensions; use subxt::config::{ @@ -11,6 +12,7 @@ pub mod runtime {} // We don't need to construct this at runtime, // so an empty enum is appropriate: +#[derive(EncodeAsType)] pub enum CustomConfig {} impl Config for CustomConfig { @@ -32,6 +34,10 @@ impl Config for CustomConfig { signed_extensions::CheckMortality, signed_extensions::ChargeAssetTxPayment, signed_extensions::ChargeTransactionPayment, + signed_extensions::SkipCheckIfFeeless< + Self, + signed_extensions::ChargeAssetTxPayment, + >, // And add a new one of our own: CustomSignedExtension, ), @@ -81,8 +87,8 @@ impl ExtrinsicParamsEncoder for CustomSignedExtension { pub fn custom( params: DefaultExtrinsicParamsBuilder, ) -> <::ExtrinsicParams as ExtrinsicParams>::OtherParams { - let (a, b, c, d, e, f, g) = params.build(); - (a, b, c, d, e, f, g, ()) + let (a, b, c, d, e, f, g, h) = params.build(); + (a, b, c, d, e, f, g, h, ()) } #[tokio::main] diff --git a/subxt/src/blocks/extrinsic_types.rs b/subxt/src/blocks/extrinsic_types.rs index 5d7549e739..070c84e6da 100644 --- a/subxt/src/blocks/extrinsic_types.rs +++ b/subxt/src/blocks/extrinsic_types.rs @@ -13,7 +13,7 @@ use crate::{ }; use crate::config::signed_extensions::{ - ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce, + ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce, SkipCheckIfFeeless, }; use crate::config::SignedExtension; use crate::dynamic::DecodedValue; @@ -685,7 +685,7 @@ impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> { /// /// Returns `None` if `tip` was not found or decoding failed. pub fn tip(&self) -> Option { - // Note: the overhead of iterating twice should be negligible. + // Note: the overhead of iterating multiple time should be negligible. self.find::() .ok() .flatten() @@ -696,6 +696,12 @@ impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> { .flatten() .map(|e| e.tip()) }) + .or_else(|| { + self.find::>>() + .ok() + .flatten() + .map(|skip_check| skip_check.inner_signed_extension().tip()) + }) } /// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension. diff --git a/subxt/src/config/default_extrinsic_params.rs b/subxt/src/config/default_extrinsic_params.rs index 880591e7f0..df78ffa34c 100644 --- a/subxt/src/config/default_extrinsic_params.rs +++ b/subxt/src/config/default_extrinsic_params.rs @@ -17,6 +17,7 @@ pub type DefaultExtrinsicParams = signed_extensions::AnyOf< signed_extensions::CheckMortality, signed_extensions::ChargeAssetTxPayment, signed_extensions::ChargeTransactionPayment, + signed_extensions::SkipCheckIfFeeless>, ), >; @@ -131,6 +132,9 @@ impl DefaultExtrinsicParamsBuilder { let charge_transaction_params = signed_extensions::ChargeTransactionPaymentParams::tip(self.tip); + let skip_check_params = + signed_extensions::SkipCheckIfFeelessParams::from(charge_asset_tx_params.clone()); + ( (), (), @@ -139,6 +143,7 @@ impl DefaultExtrinsicParamsBuilder { check_mortality_params, charge_asset_tx_params, charge_transaction_params, + skip_check_params, ) } } diff --git a/subxt/src/config/extrinsic_params.rs b/subxt/src/config/extrinsic_params.rs index 312b64a00b..f3f0cc332e 100644 --- a/subxt/src/config/extrinsic_params.rs +++ b/subxt/src/config/extrinsic_params.rs @@ -18,7 +18,19 @@ pub enum ExtrinsicParamsError { /// A signed extension was encountered that we don't know about. #[error("Error constructing extrinsic parameters: Unknown signed extension '{0}'")] UnknownSignedExtension(String), - /// Some custom error. + /// Cannot find the type id of a signed extension in the metadata. + #[error("Cannot find extension's '{0}' type id '{1} in the metadata")] + MissingTypeId(String, u32), + /// User provided a different signed extension than the one expected. + #[error("Provided a different signed extension for '{0}', the metadata expect '{1}'")] + ExpectedAnotherExtension(String, String), + /// The inner type of a signed extension is not present in the metadata. + #[error("The inner type of the signed extension '{0}' is not present in the metadata")] + MissingInnerSignedExtension(String), + /// The inner type of the signed extension is not named. + #[error("The signed extension's '{0}' type id '{1}' does not have a name in the metadata")] + ExpectedNamedTypeId(String, u32), + /// Some custom error.s #[error("Error constructing extrinsic parameters: {0}")] Custom(CustomError), } diff --git a/subxt/src/config/mod.rs b/subxt/src/config/mod.rs index 542ea9874d..bd7fd3ec39 100644 --- a/subxt/src/config/mod.rs +++ b/subxt/src/config/mod.rs @@ -18,6 +18,7 @@ pub mod substrate; use codec::{Decode, Encode}; use core::fmt::Debug; use scale_decode::DecodeAsType; +use scale_encode::EncodeAsType; use serde::{de::DeserializeOwned, Serialize}; pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder}; @@ -54,7 +55,7 @@ pub trait Config: Sized + Send + Sync + 'static { type ExtrinsicParams: ExtrinsicParams; /// This is used to identify an asset in the `ChargeAssetTxPayment` signed extension. - type AssetId: Debug + Encode + DecodeAsType; + type AssetId: Debug + Clone + Encode + DecodeAsType + EncodeAsType; } /// given some [`Config`], this return the other params needed for its `ExtrinsicParams`. diff --git a/subxt/src/config/signed_extensions.rs b/subxt/src/config/signed_extensions.rs index b8a197bca4..24f536bd72 100644 --- a/subxt/src/config/signed_extensions.rs +++ b/subxt/src/config/signed_extensions.rs @@ -9,11 +9,12 @@ use super::extrinsic_params::{ExtrinsicParams, ExtrinsicParamsEncoder, ExtrinsicParamsError}; use crate::utils::Era; -use crate::{client::OfflineClientT, Config}; +use crate::{client::OfflineClientT, Config, Metadata}; use codec::{Compact, Encode}; use core::fmt::Debug; - use scale_decode::DecodeAsType; +use scale_encode::EncodeAsType; +use std::marker::PhantomData; use std::collections::HashMap; @@ -32,7 +33,7 @@ pub trait SignedExtension: ExtrinsicParams { } /// The [`CheckSpecVersion`] signed extension. -#[derive(Debug)] +#[derive(Clone, Debug, EncodeAsType, DecodeAsType)] pub struct CheckSpecVersion(u32); impl ExtrinsicParams for CheckSpecVersion { @@ -60,7 +61,7 @@ impl SignedExtension for CheckSpecVersion { } /// The [`CheckNonce`] signed extension. -#[derive(Debug)] +#[derive(Clone, Debug, EncodeAsType, DecodeAsType)] pub struct CheckNonce(Compact); impl ExtrinsicParams for CheckNonce { @@ -88,7 +89,7 @@ impl SignedExtension for CheckNonce { } /// The [`CheckTxVersion`] signed extension. -#[derive(Debug)] +#[derive(Clone, Debug, EncodeAsType, DecodeAsType)] pub struct CheckTxVersion(u32); impl ExtrinsicParams for CheckTxVersion { @@ -116,6 +117,9 @@ impl SignedExtension for CheckTxVersion { } /// The [`CheckGenesis`] signed extension. +#[derive(Clone, EncodeAsType, DecodeAsType)] +#[decode_as_type(trait_bounds = "T::Hash: DecodeAsType")] +#[encode_as_type(trait_bounds = "T::Hash: EncodeAsType")] pub struct CheckGenesis(T::Hash); impl std::fmt::Debug for CheckGenesis { @@ -149,12 +153,16 @@ impl SignedExtension for CheckGenesis { } /// The [`CheckMortality`] signed extension. +#[derive(Clone, EncodeAsType, DecodeAsType)] +#[decode_as_type(trait_bounds = "T::Hash: DecodeAsType")] +#[encode_as_type(trait_bounds = "T::Hash: EncodeAsType")] pub struct CheckMortality { era: Era, checkpoint: T::Hash, } /// Parameters to configure the [`CheckMortality`] signed extension. +#[derive(Clone, Debug)] pub struct CheckMortalityParams { era: Era, checkpoint: Option, @@ -229,8 +237,9 @@ impl SignedExtension for CheckMortality { } /// The [`ChargeAssetTxPayment`] signed extension. -#[derive(Debug, DecodeAsType)] +#[derive(Clone, Debug, DecodeAsType, EncodeAsType)] #[decode_as_type(trait_bounds = "T::AssetId: DecodeAsType")] +#[encode_as_type(trait_bounds = "T::AssetId: EncodeAsType")] pub struct ChargeAssetTxPayment { tip: Compact, asset_id: Option, @@ -249,11 +258,22 @@ impl ChargeAssetTxPayment { } /// Parameters to configure the [`ChargeAssetTxPayment`] signed extension. +#[derive(Debug)] pub struct ChargeAssetTxPaymentParams { tip: u128, asset_id: Option, } +// Dev note: `#[derive(Clone)]` implies `T: Clone` instead of `T::AssetId: Clone`. +impl Clone for ChargeAssetTxPaymentParams { + fn clone(&self) -> Self { + Self { + tip: self.tip, + asset_id: self.asset_id.clone(), + } + } +} + impl Default for ChargeAssetTxPaymentParams { fn default() -> Self { ChargeAssetTxPaymentParams { @@ -315,7 +335,7 @@ impl SignedExtension for ChargeAssetTxPayment { } /// The [`ChargeTransactionPayment`] signed extension. -#[derive(Debug, DecodeAsType)] +#[derive(Clone, Debug, DecodeAsType, EncodeAsType)] pub struct ChargeTransactionPayment { tip: Compact, } @@ -370,6 +390,211 @@ impl SignedExtension for ChargeTransactionPayment { type Decoded = Self; } +/// Information needed to encode the [`SkipCheckIfFeeless`] signed extension. +#[derive(Debug)] +struct SkipCheckIfFeelessEncodingData { + metadata: Metadata, + type_id: u32, +} + +impl SkipCheckIfFeelessEncodingData { + /// Construct [`SkipCheckIfFeelessEncodingData`]. + fn new( + metadata: Metadata, + extension: &str, + inner_extension: &str, + ) -> Result { + let skip_check_type_id = metadata + .extrinsic() + .signed_extensions() + .iter() + .find_map(|ext| { + if ext.identifier() == extension { + Some(ext.extra_ty()) + } else { + None + } + }); + let Some(skip_check_type_id) = skip_check_type_id else { + return Err(ExtrinsicParamsError::UnknownSignedExtension( + inner_extension.to_owned(), + )); + }; + + // Ensure that the `SkipCheckIfFeeless` type has the same inner signed extension as provided. + let Some(skip_check_ty) = metadata.types().resolve(skip_check_type_id) else { + return Err(ExtrinsicParamsError::MissingTypeId( + inner_extension.to_owned(), + skip_check_type_id, + )); + }; + + // The substrate's `SkipCheckIfFeeless` contains 2 types: the inner signed extension and a phantom data. + // Phantom data does not have a type associated, so we need to find the inner signed extension. + let Some(inner_type_id) = skip_check_ty + .type_params + .iter() + .find_map(|param| param.ty.map(|ty| ty.id)) + else { + return Err(ExtrinsicParamsError::MissingInnerSignedExtension( + inner_extension.to_owned(), + )); + }; + + // Get the inner type of the `SkipCheckIfFeeless` extension to check if the naming matches the provided parameters. + let Some(inner_extension_ty) = metadata.types().resolve(inner_type_id) else { + return Err(ExtrinsicParamsError::MissingTypeId( + inner_extension.to_owned(), + inner_type_id, + )); + }; + + let Some(inner_extension_name) = inner_extension_ty.path.segments.last() else { + return Err(ExtrinsicParamsError::ExpectedNamedTypeId( + inner_extension.to_owned(), + inner_type_id, + )); + }; + + if inner_extension_name != inner_extension { + return Err(ExtrinsicParamsError::ExpectedAnotherExtension( + inner_extension.to_owned(), + inner_extension_name.to_owned(), + )); + } + + Ok(SkipCheckIfFeelessEncodingData { + metadata, + type_id: inner_type_id, + }) + } +} + +/// The [`SkipCheckIfFeeless`] signed extension. +#[derive(Debug, DecodeAsType, EncodeAsType)] +#[decode_as_type(trait_bounds = "S: DecodeAsType")] +#[encode_as_type(trait_bounds = "S: EncodeAsType")] +pub struct SkipCheckIfFeeless +where + T: Config, + S: SignedExtension + DecodeAsType + EncodeAsType, +{ + inner: S, + // Dev note: This is `Option` because `#[derive(DecodeAsType)]` requires the + // `Default` bound on skipped parameters. + // This field is populated when the [`SkipCheckIfFeeless`] is constructed from + // [`ExtrinsicParams`] (ie, when subxt submits extrinsics). However, it is not + // populated when decoding signed extensions from the node. + #[decode_as_type(skip)] + #[encode_as_type(skip)] + encoding_data: Option, + #[decode_as_type(skip)] + #[encode_as_type(skip)] + _phantom: PhantomData, +} + +impl SkipCheckIfFeeless +where + T: Config, + S: SignedExtension + DecodeAsType + EncodeAsType, +{ + /// The inner signed extension. + pub fn inner_signed_extension(&self) -> &S { + &self.inner + } +} + +impl ExtrinsicParams for SkipCheckIfFeeless +where + T: Config, + S: SignedExtension + DecodeAsType + EncodeAsType, + >::OtherParams: Default, +{ + type OtherParams = SkipCheckIfFeelessParams; + type Error = ExtrinsicParamsError; + + fn new>( + nonce: u64, + client: Client, + other_params: Self::OtherParams, + ) -> Result { + let other_params = other_params.0.unwrap_or_default(); + + let metadata = client.metadata(); + let encoding_data = SkipCheckIfFeelessEncodingData::new(metadata, Self::NAME, S::NAME)?; + let inner_extension = S::new(nonce, client, other_params).map_err(Into::into)?; + + Ok(SkipCheckIfFeeless { + inner: inner_extension, + encoding_data: Some(encoding_data), + _phantom: PhantomData, + }) + } +} + +impl ExtrinsicParamsEncoder for SkipCheckIfFeeless +where + T: Config, + S: SignedExtension + DecodeAsType + EncodeAsType, +{ + fn encode_extra_to(&self, v: &mut Vec) { + if let Some(encoding_data) = &self.encoding_data { + let _ = self.inner.encode_as_type_to( + encoding_data.type_id, + encoding_data.metadata.types(), + v, + ); + } + } +} + +impl SignedExtension for SkipCheckIfFeeless +where + T: Config, + S: SignedExtension + DecodeAsType + EncodeAsType, + >::OtherParams: Default, +{ + const NAME: &'static str = "SkipCheckIfFeeless"; + type Decoded = Self; +} + +/// Parameters to configure the [`SkipCheckIfFeeless`] signed extension. +pub struct SkipCheckIfFeelessParams(Option<>::OtherParams>) +where + T: Config, + S: SignedExtension; + +impl std::fmt::Debug for SkipCheckIfFeelessParams +where + T: Config, + S: SignedExtension, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("SkipCheckIfFeelessParams").finish() + } +} + +impl> Default for SkipCheckIfFeelessParams +where + T: Config, + S: SignedExtension, +{ + fn default() -> Self { + SkipCheckIfFeelessParams(None) + } +} + +impl SkipCheckIfFeelessParams +where + T: Config, + S: SignedExtension, +{ + /// Skip the check if the transaction is feeless. + pub fn from(extrinsic_params: >::OtherParams) -> Self { + SkipCheckIfFeelessParams(Some(extrinsic_params)) + } +} + /// This accepts a tuple of [`SignedExtension`]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. diff --git a/subxt/src/utils/era.rs b/subxt/src/utils/era.rs index 6bea303de4..a01950724a 100644 --- a/subxt/src/utils/era.rs +++ b/subxt/src/utils/era.rs @@ -3,11 +3,21 @@ // see LICENSE for license details. use scale_decode::DecodeAsType; +use scale_encode::EncodeAsType; // Dev note: This and related bits taken from `sp_runtime::generic::Era` /// An era to describe the longevity of a transaction. #[derive( - PartialEq, Default, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize, DecodeAsType, + PartialEq, + Default, + Eq, + Clone, + Copy, + Debug, + serde::Serialize, + serde::Deserialize, + DecodeAsType, + EncodeAsType, )] pub enum Era { /// The transaction is valid forever. The genesis hash must be present in the signed content. diff --git a/testing/integration-tests/src/full_client/blocks/mod.rs b/testing/integration-tests/src/full_client/blocks/mod.rs index 53c6d4d9a2..8485f1f4cd 100644 --- a/testing/integration-tests/src/full_client/blocks/mod.rs +++ b/testing/integration-tests/src/full_client/blocks/mod.rs @@ -5,7 +5,9 @@ use crate::{test_context, utils::node_runtime}; use codec::{Compact, Encode}; use futures::StreamExt; -use subxt::config::signed_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce}; +use subxt::config::signed_extensions::{ + ChargeAssetTxPayment, CheckMortality, CheckNonce, SkipCheckIfFeeless, +}; use subxt::config::DefaultExtrinsicParamsBuilder; use subxt::config::SubstrateConfig; use subxt::utils::Era; @@ -276,13 +278,15 @@ async fn decode_signed_extensions_from_blocks() { let transaction1 = submit_transfer_extrinsic_and_get_it_back!(1234); let extensions1 = transaction1.signed_extensions().unwrap(); + let nonce1 = extensions1.nonce().unwrap(); let nonce1_static = extensions1.find::().unwrap().unwrap().0; let tip1 = extensions1.tip().unwrap(); let tip1_static: u128 = extensions1 - .find::>() + .find::>>() .unwrap() .unwrap() + .inner_signed_extension() .tip(); let transaction2 = submit_transfer_extrinsic_and_get_it_back!(5678); @@ -291,9 +295,10 @@ async fn decode_signed_extensions_from_blocks() { let nonce2_static = extensions2.find::().unwrap().unwrap().0; let tip2 = extensions2.tip().unwrap(); let tip2_static: u128 = extensions2 - .find::>() + .find::>>() .unwrap() .unwrap() + .inner_signed_extension() .tip(); assert_eq!(nonce1, 0); @@ -313,7 +318,7 @@ async fn decode_signed_extensions_from_blocks() { "CheckMortality", "CheckNonce", "CheckWeight", - "ChargeAssetTxPayment", + "SkipCheckIfFeeless", ]; assert_eq!(extensions1.iter().count(), expected_signed_extensions.len());