diff --git a/.gitignore b/.gitignore index 0dde365146..2b21ebcb92 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ cargo-timing* /examples/parachain-example/target /examples/parachain-example/metadata/target .vscode +.claude \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0e07690990..282bd48734 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1959,9 +1959,9 @@ dependencies = [ [[package]] name = "frame-decode" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63257bb5f8d7a707d626aa1b4e464c3b9720edd168b73cee98f48f0dd6386e4" +checksum = "dea4ae9db992bb3d089885a4fc17d06ffbd6918d7434fd192a20aba02d554bff" dependencies = [ "frame-metadata 23.0.0", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index 54dab20f1e..0edaa355be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ darling = "0.20.10" derive-where = "1.2.7" either = { version = "1.13.0", default-features = false } finito = { version = "0.1.0", default-features = false } -frame-decode = { version = "0.16.1", default-features = false } +frame-decode = { version = "0.17.0", default-features = false } frame-metadata = { version = "23.0.0", default-features = false } futures = { version = "0.3.31", default-features = false, features = ["std"] } getrandom = { version = "0.2", default-features = false } diff --git a/README.md b/README.md index a690c84334..014b638483 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Subxt is a library for interacting with chains in the [Polkadot](https://github. - Do all of the above via a safe, statically typed interface or via a flexible dynamic interface. - Do most of the above via a built-in light client to interact with chains trustlessly. - Compile to WASM and run [entirely in the browser](./examples/wasm-example), or be [called via FFI](./examples/ffi-example) in many other languages. +- Be used entirely offline to provide a subset of the available functionality. ## Usage diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index 5041066e0c..bc5de59ca8 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -381,7 +381,7 @@ fn default_substitutes(crate_path: &syn::Path) -> TypeSubstitutes { ), ( parse_quote!(fp_account::AccountId20), - parse_quote!(#crate_path::utils::AccountId20), + parse_quote!(#crate_path::utils::eth::AccountId20), ), ( parse_quote!(sp_runtime::multiaddress::MultiAddress), diff --git a/examples/ffi-example/Cargo.lock b/examples/ffi-example/Cargo.lock index 580621258a..ec7655bad8 100644 --- a/examples/ffi-example/Cargo.lock +++ b/examples/ffi-example/Cargo.lock @@ -889,9 +889,9 @@ dependencies = [ [[package]] name = "frame-decode" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63257bb5f8d7a707d626aa1b4e464c3b9720edd168b73cee98f48f0dd6386e4" +checksum = "dea4ae9db992bb3d089885a4fc17d06ffbd6918d7434fd192a20aba02d554bff" dependencies = [ "frame-metadata", "parity-scale-codec", @@ -2298,9 +2298,9 @@ dependencies = [ [[package]] name = "scale-info-legacy" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afb76e1b2cb589b97278e2f1e2e290c9b7c51d6ac69afab9e1d7d1e136a9276" +checksum = "d972ce93a4f81efc40fce251e992faf99ecb090c02bcbd3614e213991038c181" dependencies = [ "hashbrown 0.16.1", "scale-type-resolver", diff --git a/examples/parachain-example/Cargo.lock b/examples/parachain-example/Cargo.lock index 48dfd9a6e4..f0174feec0 100644 --- a/examples/parachain-example/Cargo.lock +++ b/examples/parachain-example/Cargo.lock @@ -945,9 +945,9 @@ dependencies = [ [[package]] name = "frame-decode" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63257bb5f8d7a707d626aa1b4e464c3b9720edd168b73cee98f48f0dd6386e4" +checksum = "dea4ae9db992bb3d089885a4fc17d06ffbd6918d7434fd192a20aba02d554bff" dependencies = [ "frame-metadata", "parity-scale-codec", @@ -2374,9 +2374,9 @@ dependencies = [ [[package]] name = "scale-info-legacy" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afb76e1b2cb589b97278e2f1e2e290c9b7c51d6ac69afab9e1d7d1e136a9276" +checksum = "d972ce93a4f81efc40fce251e992faf99ecb090c02bcbd3614e213991038c181" dependencies = [ "hashbrown 0.16.1", "scale-type-resolver", diff --git a/examples/wasm-example/Cargo.lock b/examples/wasm-example/Cargo.lock index 79a4229723..cfcc1b831f 100644 --- a/examples/wasm-example/Cargo.lock +++ b/examples/wasm-example/Cargo.lock @@ -719,9 +719,9 @@ dependencies = [ [[package]] name = "frame-decode" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63257bb5f8d7a707d626aa1b4e464c3b9720edd168b73cee98f48f0dd6386e4" +checksum = "dea4ae9db992bb3d089885a4fc17d06ffbd6918d7434fd192a20aba02d554bff" dependencies = [ "frame-metadata", "parity-scale-codec", @@ -2170,9 +2170,9 @@ dependencies = [ [[package]] name = "scale-info-legacy" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afb76e1b2cb589b97278e2f1e2e290c9b7c51d6ac69afab9e1d7d1e136a9276" +checksum = "d972ce93a4f81efc40fce251e992faf99ecb090c02bcbd3614e213991038c181" dependencies = [ "hashbrown 0.16.1", "scale-type-resolver", diff --git a/examples/wasm-example/src/routes/signing.rs b/examples/wasm-example/src/routes/signing.rs index 5dc8b4ea27..a9ff1123ba 100644 --- a/examples/wasm-example/src/routes/signing.rs +++ b/examples/wasm-example/src/routes/signing.rs @@ -5,7 +5,6 @@ use subxt::{client::OnlineClientAtBlockImpl, OnlineClient, OnlineClientAtBlock, use subxt::config::DefaultExtrinsicParamsBuilder; use subxt::ext::codec::{Decode, Encode}; -use subxt::tx::Payload as _; use subxt::tx::SubmittableTransaction; use subxt::utils::{AccountId32, MultiSignature}; @@ -26,10 +25,7 @@ impl SigningExamplesComponent { fn set_message(&mut self, message: String) { let remark_call = polkadot::tx().system().remark(message.as_bytes().to_vec()); let online_client_at_block = self.online_client.as_ref().unwrap(); - let remark_call_bytes = remark_call - .encode_call_data(online_client_at_block.metadata_ref()) - .unwrap(); - self.remark_call_bytes = remark_call_bytes; + self.remark_call_bytes = online_client_at_block.tx().call_data(&remark_call).unwrap(); self.message = message; } } @@ -182,8 +178,9 @@ impl Component for SigningExamplesComponent { }; // Apply the signature - let signed_extrinsic = - signable.sign_with_account_and_signature(&account_id, &multi_signature); + let Ok(signed_extrinsic) = signable.sign_with_account_and_signature(&account_id, &multi_signature) else { + return Message::Error(anyhow!("could not sign the extrinsic")); + }; // check the TX validity (to debug in the js console if the extrinsic would work) let dry_res = signed_extrinsic.validate().await; diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index da20fe5fc0..1479200bd0 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -30,8 +30,8 @@ use alloc::vec::Vec; use frame_decode::constants::{ConstantEntry, ConstantInfo, ConstantInfoError}; use frame_decode::custom_values::{CustomValue, CustomValueInfo, CustomValueInfoError}; use frame_decode::extrinsics::{ - ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicInfoArg, ExtrinsicInfoError, - ExtrinsicSignatureInfo, + ExtrinsicCallInfo, ExtrinsicCallInfoArg, ExtrinsicExtensionInfo, ExtrinsicExtensionInfoArg, + ExtrinsicInfoError, ExtrinsicSignatureInfo, }; use frame_decode::runtime_apis::{ RuntimeApiEntry, RuntimeApiInfo, RuntimeApiInfoError, RuntimeApiInput, @@ -99,7 +99,7 @@ pub struct Metadata { impl frame_decode::extrinsics::ExtrinsicTypeInfo for Metadata { type TypeId = u32; - fn extrinsic_call_info( + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, @@ -119,12 +119,49 @@ impl frame_decode::extrinsics::ExtrinsicTypeInfo for Metadata { })?; Ok(ExtrinsicCallInfo { + call_index, + pallet_index, pallet_name: Cow::Borrowed(pallet.name()), call_name: Cow::Borrowed(&call.name), args: call .fields .iter() - .map(|f| ExtrinsicInfoArg { + .map(|f| ExtrinsicCallInfoArg { + name: Cow::Borrowed(f.name.as_deref().unwrap_or("")), + id: f.ty.id, + }) + .collect(), + }) + } + + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>> { + let pallet = self.pallet_by_name(pallet_name).ok_or({ + ExtrinsicInfoError::PalletNotFoundByName { + name: Cow::Owned(pallet_name.to_string()), + } + })?; + + let call = pallet.call_variant_by_name(call_name).ok_or_else(|| { + ExtrinsicInfoError::CallNotFoundByName { + pallet_index: pallet.call_index(), + pallet_name: Cow::Borrowed(pallet.name()), + call_name: Cow::Owned(call_name.to_string()), + } + })?; + + Ok(ExtrinsicCallInfo { + call_index: call.index, + pallet_index: pallet.call_index(), + pallet_name: Cow::Borrowed(pallet.name()), + call_name: Cow::Borrowed(&call.name), + args: call + .fields + .iter() + .map(|f| ExtrinsicCallInfoArg { name: Cow::Borrowed(f.name.as_deref().unwrap_or("")), id: f.ty.id, }) @@ -141,6 +178,16 @@ impl frame_decode::extrinsics::ExtrinsicTypeInfo for Metadata { }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(self + .extrinsic + .transaction_extensions_by_version + .keys() + .copied()) + } + fn extrinsic_extension_info( &self, extension_version: Option, @@ -157,9 +204,10 @@ impl frame_decode::extrinsics::ExtrinsicTypeInfo for Metadata { .extrinsic() .transaction_extensions_by_version(extension_version) .ok_or(ExtrinsicInfoError::ExtrinsicExtensionVersionNotFound { extension_version })? - .map(|f| ExtrinsicInfoArg { + .map(|f| ExtrinsicExtensionInfoArg { name: Cow::Borrowed(f.identifier()), id: f.extra_ty(), + implicit_id: f.additional_ty(), }) .collect(); diff --git a/signer/src/eth.rs b/signer/src/eth.rs index a865efc355..8e52e1e4f1 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -311,8 +311,20 @@ mod subxt_compat { use super::*; use subxt::config::Config; use subxt::transactions::Signer as SignerT; - use subxt::utils::AccountId20; use subxt::utils::MultiAddress; + use subxt::utils::eth::AccountId20; + + impl From for subxt::utils::eth::Signature { + fn from(value: Signature) -> Self { + subxt::utils::eth::Signature(value.0) + } + } + + impl From for Signature { + fn from(value: subxt::utils::eth::Signature) -> Self { + Signature(value.0) + } + } impl SignerT for Keypair where @@ -367,7 +379,7 @@ mod test { use secp256k1::Secp256k1; use subxt::config::{Config, HashFor, substrate}; use subxt::transactions::Signer as SignerT; - use subxt::utils::AccountId20; + use subxt::utils::eth::AccountId20; use super::*; @@ -377,10 +389,10 @@ mod test { impl Config for StubEthRuntimeConfig { type AccountId = AccountId20; type Address = AccountId20; - type Signature = Signature; + type Signature = subxt::utils::eth::Signature; type Hasher = substrate::BlakeTwo256; type Header = substrate::SubstrateHeader>; - type ExtrinsicParams = substrate::SubstrateExtrinsicParams; + type TransactionExtensions = substrate::SubstrateExtrinsicParams; type AssetId = u32; } @@ -439,7 +451,7 @@ mod test { let msg_as_bytes = msg.as_bytes(); assert_eq!(SubxtSigner::account_id(&keypair), keypair.public_key().to_account_id()); - assert_eq!(SubxtSigner::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes)); + assert_eq!(SubxtSigner::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes).into()); } #[test] @@ -459,7 +471,7 @@ mod test { #[test] fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") { - let sig = SubxtSigner::sign(&keypair, msg.as_bytes()); + let sig = SubxtSigner::sign(&keypair, msg.as_bytes()).into(); assert!(verify( &sig, diff --git a/subxt/examples/config_assethub.rs b/subxt/examples/config_assethub.rs index 90ad4c276c..5dc83b28c5 100644 --- a/subxt/examples/config_assethub.rs +++ b/subxt/examples/config_assethub.rs @@ -1,7 +1,8 @@ //! Configuring Subxt to talk to AssetHub. use subxt::Error; use subxt::config::{ - Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, PolkadotConfig, SubstrateConfig, + Config, DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions, PolkadotConfig, + SubstrateConfig, }; use subxt_signer::sr25519::dev; @@ -34,7 +35,7 @@ impl Config for AssetHubConfig { type Signature = ::Signature; type Hasher = ::Hasher; type Header = ::Header; - type ExtrinsicParams = DefaultExtrinsicParams; + type TransactionExtensions = DefaultTransactionExtensions; // Forward these methods to the default SubstrateConfig: fn genesis_hash(&self) -> Option> { diff --git a/subxt/examples/config_eth.rs b/subxt/examples/config_eth.rs index 39057c4f57..768a468c94 100644 --- a/subxt/examples/config_eth.rs +++ b/subxt/examples/config_eth.rs @@ -1,6 +1,6 @@ //! Configuring Subxt to talk to AssetHub. use subxt::Error; -use subxt::config::{Config, DefaultExtrinsicParams, SubstrateConfig}; +use subxt::config::{Config, DefaultTransactionExtensions, SubstrateConfig}; /// Our EthConfig wraps SubstrateConfig and configures the /// account ID / address / signature to be based on 20 byte IDs. @@ -8,16 +8,17 @@ use subxt::config::{Config, DefaultExtrinsicParams, SubstrateConfig}; pub struct EthConfig(SubstrateConfig); impl Config for EthConfig { - // AssetHub, like Polkadot, has no account index on its address type: - type Address = subxt::utils::AccountId20; - type AccountId = subxt::utils::AccountId20; - type Signature = subxt_signer::eth::Signature; + // Eth based chains use 20 byte account IDs + // and ecdsa based signing: + type Address = subxt::utils::eth::AccountId20; + type AccountId = subxt::utils::eth::AccountId20; + type Signature = subxt::utils::eth::Signature; // Just copy the default SubstrateConfig for these: type AssetId = ::AssetId; type Hasher = ::Hasher; type Header = ::Header; - type ExtrinsicParams = DefaultExtrinsicParams; + type TransactionExtensions = DefaultTransactionExtensions; // Forward these methods to the default SubstrateConfig: fn genesis_hash(&self) -> Option> { diff --git a/subxt/examples/light_client.rs b/subxt/examples/light_client.rs index 4df38ee76f..9967b91c1c 100644 --- a/subxt/examples/light_client.rs +++ b/subxt/examples/light_client.rs @@ -45,7 +45,12 @@ async fn main() -> Result<(), Box> { let block = block?; // Print some details about the blocks we fetch via the light client. - println!("Chain {:?} hash={:?}", chain, block.hash()); + println!( + "Chain {:?} height={:?} hash={:?}", + chain, + block.number(), + block.hash() + ); let at_block = block.at().await?; let extrinsics = at_block.extrinsics().fetch().await?; for ext in extrinsics.iter() { diff --git a/subxt/src/config.rs b/subxt/src/config.rs index c02b022e1c..90d7c9efed 100644 --- a/subxt/src/config.rs +++ b/subxt/src/config.rs @@ -45,9 +45,9 @@ //! configured. Please open an issue in the Subxt repository and we can help to narrow down what //! the problem might be. -mod default_extrinsic_params; +mod default_transaction_extensions; +mod transaction_extension_traits; -pub mod extrinsic_params; pub mod polkadot; pub mod substrate; pub mod transaction_extensions; @@ -62,11 +62,12 @@ use serde::{Serialize, de::DeserializeOwned}; use std::{fmt::Display, marker::PhantomData}; use subxt_rpcs::RpcConfig; -pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder}; -pub use extrinsic_params::{ClientState, ExtrinsicParams, ExtrinsicParamsEncoder}; +pub use default_transaction_extensions::{ + DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions, +}; pub use polkadot::{PolkadotConfig, PolkadotExtrinsicParams, PolkadotExtrinsicParamsBuilder}; pub use substrate::{SubstrateConfig, SubstrateExtrinsicParams, SubstrateExtrinsicParamsBuilder}; -pub use transaction_extensions::TransactionExtension; +pub use transaction_extension_traits::{ClientState, TransactionExtension, TransactionExtensions}; /// Configuration for a given chain and the runtimes within. This consists of the /// type information needed to work at the head of the chain (namely submitting @@ -74,19 +75,19 @@ pub use transaction_extensions::TransactionExtension; /// given chain. pub trait Config: Clone + Debug + Sized + Send + Sync + 'static { /// The account ID type; required for constructing extrinsics. - type AccountId: Debug + Clone + Encode + Decode + Serialize + Send; + type AccountId: Debug + Clone + Encode + EncodeAsType + DecodeAsType + Serialize + Send; /// The address type; required for constructing extrinsics. - type Address: Debug + Encode + From; + type Address: Debug + EncodeAsType + From; /// The signature type. - type Signature: Debug + Clone + Encode + Decode + Send; + type Signature: Debug + Clone + EncodeAsType + DecodeAsType + Send; /// The block header. type Header: Header; /// This type defines the extrinsic extra and additional parameters. - type ExtrinsicParams: ExtrinsicParams; + type TransactionExtensions: TransactionExtensions; /// This is used to identify an asset in the `ChargeAssetTxPayment` signed extension. type AssetId: AssetId; @@ -160,7 +161,7 @@ impl RpcConfig for RpcConfigFor { pub type HashFor = <::Hasher as Hasher>::Hash; /// given some [`Config`], this return the other params needed for its `ExtrinsicParams`. -pub type ParamsFor = <::ExtrinsicParams as ExtrinsicParams>::Params; +pub type ParamsFor = <::TransactionExtensions as TransactionExtensions>::Params; /// AssetId types must conform to this trait. pub trait AssetId: Debug + Clone + Encode + DecodeAsType + EncodeAsType + Send {} diff --git a/subxt/src/config/default_extrinsic_params.rs b/subxt/src/config/default_transaction_extensions.rs similarity index 84% rename from subxt/src/config/default_extrinsic_params.rs rename to subxt/src/config/default_transaction_extensions.rs index fc871b7f3b..1ee7b4bdc5 100644 --- a/subxt/src/config/default_extrinsic_params.rs +++ b/subxt/src/config/default_transaction_extensions.rs @@ -2,31 +2,28 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. +use super::{Config, HashFor}; +use super::{TransactionExtensions, transaction_extensions}; use crate::config::transaction_extensions::CheckMortalityParams; -use super::{Config, HashFor}; -use super::{ExtrinsicParams, transaction_extensions}; - -/// The default [`super::ExtrinsicParams`] implementation understands common signed extensions -/// and how to apply them to a given chain. -pub type DefaultExtrinsicParams = transaction_extensions::AnyOf< - T, - ( - 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, - ), ->; - -/// A builder that outputs the set of [`super::ExtrinsicParams::Params`] required for -/// [`DefaultExtrinsicParams`]. This may expose methods that aren't applicable to the current -/// chain; such values will simply be ignored if so. +/// A set of transaction extensions which is applicable in most default cases. This can be used +/// as our [`Config::TransactionExtensions`] type, and then users can configure transactions by +/// using the corresponding [`DefaultExtrinsicParamsBuilder`]. +pub type DefaultTransactionExtensions = ( + 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, +); + +/// A builder that outputs the set of parameters required to configure transactions when +/// [`DefaultTransactionExtensions`] is used. 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, else it's mortality is described. mortality: transaction_extensions::CheckMortalityParams, @@ -123,7 +120,7 @@ impl DefaultExtrinsicParamsBuilder { } /// Build the extrinsic parameters. - pub fn build(self) -> as ExtrinsicParams>::Params { + pub fn build(self) -> as TransactionExtensions>::Params { let check_mortality_params = self.mortality; let charge_asset_tx_params = if let Some(asset_id) = self.tip_of_asset_id { diff --git a/subxt/src/config/extrinsic_params.rs b/subxt/src/config/extrinsic_params.rs deleted file mode 100644 index fa3cdcb11b..0000000000 --- a/subxt/src/config/extrinsic_params.rs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2019-2026 Parity Technologies (UK) Ltd. -// This file is dual-licensed as Apache-2.0 or GPL-3.0. -// see LICENSE for license details. - -//! This module contains a trait which controls the parameters that must -//! be provided in order to successfully construct an extrinsic. -//! [`crate::config::DefaultExtrinsicParams`] provides a general-purpose -//! implementation of this that will work in many cases. - -use crate::config::{Config, HashFor}; -use crate::error::ExtrinsicParamsError; -use crate::metadata::ArcMetadata; -use core::any::Any; - -/// This provides access to some relevant client state in transaction extensions, -/// and is just a combination of some of the available properties. -#[derive(Clone, Debug)] -pub struct ClientState { - /// Genesis hash. - pub genesis_hash: HashFor, - /// Spec version. - pub spec_version: u32, - /// Transaction version. - pub transaction_version: u32, - /// Metadata. - pub metadata: ArcMetadata, -} - -/// This trait allows you to configure the "signed extra" and -/// "additional" parameters that are a part of the transaction payload -/// or the signer payload respectively. -pub trait ExtrinsicParams: ExtrinsicParamsEncoder + Sized + Send + 'static { - /// 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: Params; - - /// Construct a new instance of our [`ExtrinsicParams`]. - fn new(client: &ClientState, params: Self::Params) -> Result; -} - -/// This trait is expected to be implemented for any [`ExtrinsicParams`], and -/// 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 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) {} - - /// 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_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: HashFor) {} -} - -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: HashFor) { - $(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, T 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, T 19, U 20); - 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, T 19, U 20, V 21); - 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, T 19, U 20, V 21, W 22); - 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, T 19, U 20, V 21, W 22, X 23); - 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, T 19, U 20, V 21, W 22, X 23, Y 24); - 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, T 19, U 20, V 21, W 22, X 23, Y 24, Z 25); -}; diff --git a/subxt/src/config/polkadot.rs b/subxt/src/config/polkadot.rs index 5493084cbf..f6dfb7a066 100644 --- a/subxt/src/config/polkadot.rs +++ b/subxt/src/config/polkadot.rs @@ -4,7 +4,7 @@ //! Polkadot specific configuration -use super::{Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder}; +use super::{Config, DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions}; use crate::config::substrate::{SubstrateConfig, SubstrateConfigBuilder}; use crate::metadata::ArcMetadata; @@ -123,7 +123,7 @@ impl Config for PolkadotConfig { // These are the same as the default substrate node, but redefined // because we need to pass the PolkadotConfig trait as a param. - type ExtrinsicParams = PolkadotExtrinsicParams; + type TransactionExtensions = PolkadotExtrinsicParams; fn genesis_hash(&self) -> Option> { self.0.genesis_hash() @@ -152,7 +152,7 @@ impl Config for PolkadotConfig { /// A struct representing the signed extra and additional parameters required /// to construct a transaction for a polkadot node. -pub type PolkadotExtrinsicParams = DefaultExtrinsicParams; +pub type PolkadotExtrinsicParams = DefaultTransactionExtensions; /// A builder which leads to [`PolkadotExtrinsicParams`] being constructed. /// This is what you provide to methods like `sign_and_submit()`. diff --git a/subxt/src/config/substrate.rs b/subxt/src/config/substrate.rs index 711d4998a0..ebb8131370 100644 --- a/subxt/src/config/substrate.rs +++ b/subxt/src/config/substrate.rs @@ -4,7 +4,7 @@ //! Substrate specific configuration -use super::{Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, Hasher, Header}; +use super::{Config, DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions, Hasher, Header}; use crate::config::Hash; use crate::metadata::{ArcMetadata, Metadata}; use crate::utils::RangeMap; @@ -162,7 +162,7 @@ impl Config for SubstrateConfig { type Signature = MultiSignature; type Hasher = DynamicHasher256; type Header = SubstrateHeader<::Hash>; - type ExtrinsicParams = SubstrateExtrinsicParams; + type TransactionExtensions = SubstrateExtrinsicParams; type AssetId = u32; fn legacy_types_for_spec_version(&'_ self, spec_version: u32) -> Option> { @@ -202,7 +202,7 @@ impl Config for SubstrateConfig { /// A struct representing the signed extra and additional parameters required /// to construct a transaction for the default substrate node. -pub type SubstrateExtrinsicParams = DefaultExtrinsicParams; +pub type SubstrateExtrinsicParams = DefaultTransactionExtensions; /// A builder which leads to [`SubstrateExtrinsicParams`] being constructed. /// This is what you provide to methods like `sign_and_submit()`. diff --git a/subxt/src/config/transaction_extension_traits.rs b/subxt/src/config/transaction_extension_traits.rs new file mode 100644 index 0000000000..e0ed7fb496 --- /dev/null +++ b/subxt/src/config/transaction_extension_traits.rs @@ -0,0 +1,180 @@ +// Copyright 2019-2026 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +use crate::config::{Config, HashFor}; +use crate::error::TransactionExtensionError; +use crate::metadata::ArcMetadata; +use scale_decode::DecodeAsType; +use scale_info::PortableRegistry; + +/// A set of [`TransactionExtensions`]. This: +/// - Implements [`frame_decode::extrinsics::TransactionExtensions`], meaning that it +/// can be encoded to value or implicit bytes. +/// - Accepts custom params on construction, allowing user configuration to be provided. +/// - Can have an account ID and signature injected (useful for V5 transaction extensions). +pub trait TransactionExtensions: + frame_decode::extrinsics::TransactionExtensions + Sized +{ + /// These parameters can be provided to the constructor along with + /// some default parameters that Subxt understands. + type Params: Params; + + /// Construct a new instance of our [`TransactionExtensions`]. + fn new( + client: &ClientState, + params: Self::Params, + ) -> Result; + + /// Set the signature and account ID for any transaction extensions that care. + fn inject_signature(&mut self, account_id: &T::AccountId, signature: &T::Signature); +} + +/// A single transaction extension. +pub trait TransactionExtension: + frame_decode::extrinsics::TransactionExtension + Sized +{ + /// The type representing the `extra` / value bytes of a transaction extension. + /// Decoding from this type should be symmetrical to the respective + /// [`frame_decode::extrinsics::TransactionExtension::encode_value_to()`] implementation + /// for this transaction extension. + type Decoded: DecodeAsType; + + /// These parameters can be provided to the constructor along with + /// some default parameters that Subxt understands. + type Params: Params; + + /// Construct a new instance of our [`TransactionExtension`]. + fn new( + client: &ClientState, + params: Self::Params, + ) -> Result; + + /// Set the signature and accountID for this transaction extension. Defaults to doing nothing. + fn inject_signature(&mut self, _account_id: &T::AccountId, _signature: &T::Signature) {} +} + +/// This provides access to some relevant client state in transaction extensions, +/// and is just a combination of some of the available properties. +#[derive(Clone, Debug)] +pub struct ClientState { + /// Genesis hash. + pub genesis_hash: HashFor, + /// Spec version. + pub spec_version: u32, + /// Transaction version. + pub transaction_version: u32, + /// Metadata. + pub metadata: ArcMetadata, +} + +/// The parameters (ie [`TransactionExtensions::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: HashFor) {} +} + +// empty tuples impl Params and do nothing. +impl Params for () {} + +// tuples of Params are also valid Params. +macro_rules! impl_params_tuple { + ($($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: HashFor) { + $(self.$index.inject_block(number, hash);)+ + } + } + } +} + +#[rustfmt::skip] +const _: () = { + impl_params_tuple!(A 0); + impl_params_tuple!(A 0, B 1); + impl_params_tuple!(A 0, B 1, C 2); + impl_params_tuple!(A 0, B 1, C 2, D 3); + impl_params_tuple!(A 0, B 1, C 2, D 3, E 4); + impl_params_tuple!(A 0, B 1, C 2, D 3, E 4, F 5); + impl_params_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6); + impl_params_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7); + impl_params_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8); + impl_params_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9); + impl_params_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10); + impl_params_tuple!(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_params_tuple!(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_params_tuple!(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_params_tuple!(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_params_tuple!(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_params_tuple!(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_params_tuple!(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_params_tuple!(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_params_tuple!(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, T 19); + impl_params_tuple!(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, T 19, U 20); + impl_params_tuple!(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, T 19, U 20, V 21); + impl_params_tuple!(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, T 19, U 20, V 21, W 22); + impl_params_tuple!(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, T 19, U 20, V 21, W 22, X 23); + impl_params_tuple!(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, T 19, U 20, V 21, W 22, X 23, Y 24); + impl_params_tuple!(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, T 19, U 20, V 21, W 22, X 23, Y 24, Z 25); +}; + +// tuples of TransactionExtension types are automatically TransactionExtensions. +macro_rules! impl_extensions_tuple { + ($($ident:ident $index:tt),+) => { + impl),+> + TransactionExtensions for ($($ident,)+) + where + ($($ident::Params,)+): Params, + { + type Params = ($($ident::Params,)+); + + fn new(client: &ClientState, params: Self::Params) -> Result { + Ok(( + $($ident::new(client, params.$index)?,)+ + )) + } + + fn inject_signature(&mut self, account_id: &Conf::AccountId, signature: &Conf::Signature) { + $(self.$index.inject_signature(account_id, signature);)+ + } + } + } +} + +#[rustfmt::skip] +const _: () = { + impl_extensions_tuple!(A 0); + impl_extensions_tuple!(A 0, B 1); + impl_extensions_tuple!(A 0, B 1, C 2); + impl_extensions_tuple!(A 0, B 1, C 2, D 3); + impl_extensions_tuple!(A 0, B 1, C 2, D 3, E 4); + impl_extensions_tuple!(A 0, B 1, C 2, D 3, E 4, F 5); + impl_extensions_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6); + impl_extensions_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7); + impl_extensions_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8); + impl_extensions_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9); + impl_extensions_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10); + impl_extensions_tuple!(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_extensions_tuple!(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_extensions_tuple!(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_extensions_tuple!(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_extensions_tuple!(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_extensions_tuple!(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_extensions_tuple!(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_extensions_tuple!(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_extensions_tuple!(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, T 19); + impl_extensions_tuple!(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, T 19, U 20); + impl_extensions_tuple!(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, T 19, U 20, V 21); + impl_extensions_tuple!(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, T 19, U 20, V 21, W 22); + impl_extensions_tuple!(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, T 19, U 20, V 21, W 22, X 23); + impl_extensions_tuple!(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, T 19, U 20, V 21, W 22, X 23, Y 24); + impl_extensions_tuple!(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, T 19, U 20, V 21, W 22, X 23, Y 24, Z 25); +}; diff --git a/subxt/src/config/transaction_extensions.rs b/subxt/src/config/transaction_extensions.rs index 4f1e26e22d..4063a24351 100644 --- a/subxt/src/config/transaction_extensions.rs +++ b/subxt/src/config/transaction_extensions.rs @@ -3,40 +3,24 @@ // see LICENSE for license details. //! 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::{ClientState, ExtrinsicParams}; -use crate::config::ExtrinsicParamsEncoder; -use crate::config::{Config, HashFor}; -use crate::error::ExtrinsicParamsError; -use crate::utils::{Era, Static}; +//! of which implements [`TransactionExtension`]. A tuple of these can be provided +//! to [`crate::Config::TransactionExtensions`] to select those that we want to +//! make available for a given chain. + +use crate::config::transaction_extension_traits::TransactionExtension; +use crate::config::{ClientState, Config, HashFor}; +use crate::error::TransactionExtensionError; +use crate::utils::Era; use codec::{Compact, Encode}; -use core::any::Any; use core::fmt::Debug; use derive_where::derive_where; use scale_decode::DecodeAsType; +use scale_encode::EncodeAsType; use scale_info::PortableRegistry; -use std::collections::HashMap; -// 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 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_value_to()` implementation of this transaction extension. - type Decoded: DecodeAsType; - - /// 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; -} +// Re-export this here because it's a nicer public interface, and "Params" isn't overly +// specific as a name otherwise. +pub use super::transaction_extension_traits::Params; /// 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 @@ -44,57 +28,71 @@ pub trait TransactionExtension: ExtrinsicParams { /// this one in the list. pub struct VerifySignature(VerifySignatureDetails); -impl ExtrinsicParams for VerifySignature { +impl TransactionExtension for VerifySignature { + type Decoded = VerifySignatureDetails; type Params = (); - fn new(_client: &ClientState, _params: Self::Params) -> Result { + fn new( + _client: &ClientState, + _params: Self::Params, + ) -> Result { Ok(VerifySignature(VerifySignatureDetails::Disabled)) } + + fn inject_signature(&mut self, account: &T::AccountId, signature: &T::Signature) { + // The signature is not set through params, only here, once given by a user: + self.0 = VerifySignatureDetails::Signed { + signature: signature.clone(), + account: account.clone(), + } + } } -impl ExtrinsicParamsEncoder for VerifySignature { - fn encode_value_to(&self, v: &mut Vec) { - self.0.encode_to(v); +impl frame_decode::extrinsics::TransactionExtension + for VerifySignature +{ + const NAME: &str = "VerifySignature"; + + fn encode_value_to( + &self, + type_id: u32, + type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + self.0.encode_as_type_to(type_id, type_resolver, v)?; + Ok(()) } - fn encode_signer_payload_value_to(&self, v: &mut Vec) { + fn encode_value_for_signer_payload_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { // 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) { + Ok(()) + } + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { // 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::AccountId 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" + Ok(()) } } /// 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)] +#[derive(EncodeAsType, DecodeAsType)] +#[decode_as_type(trait_bounds = "")] +#[encode_as_type(trait_bounds = "")] pub enum VerifySignatureDetails { /// A signature has been provided. Signed { @@ -113,30 +111,41 @@ pub struct CheckMetadataHash { // but for now we never provide a hash and so this is empty. } -impl ExtrinsicParams for CheckMetadataHash { +impl TransactionExtension for CheckMetadataHash { + type Decoded = CheckMetadataHashMode; type Params = (); - fn new(_client: &ClientState, _params: Self::Params) -> Result { + fn new( + _client: &ClientState, + _params: Self::Params, + ) -> Result { Ok(CheckMetadataHash {}) } } -impl ExtrinsicParamsEncoder for CheckMetadataHash { - fn encode_value_to(&self, v: &mut Vec) { +impl frame_decode::extrinsics::TransactionExtension for CheckMetadataHash { + const NAME: &str = "CheckMetadataHash"; + + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { // 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_implicit_to(&self, v: &mut Vec) { + Ok(()) + } + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { // We provide no metadata hash in the signer payload to align with the above. None::<()>.encode_to(v); - } -} - -impl TransactionExtension for CheckMetadataHash { - type Decoded = CheckMetadataHashMode; - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "CheckMetadataHash" + Ok(()) } } @@ -165,48 +174,74 @@ impl CheckMetadataHashMode { /// The [`CheckSpecVersion`] transaction extension. pub struct CheckSpecVersion(u32); -impl ExtrinsicParams for CheckSpecVersion { +impl TransactionExtension for CheckSpecVersion { + type Decoded = (); type Params = (); - fn new(client: &ClientState, _params: Self::Params) -> Result { + fn new( + client: &ClientState, + _params: Self::Params, + ) -> Result { Ok(CheckSpecVersion(client.spec_version)) } } -impl ExtrinsicParamsEncoder for CheckSpecVersion { - fn encode_implicit_to(&self, v: &mut Vec) { - self.0.encode_to(v); - } -} +impl frame_decode::extrinsics::TransactionExtension for CheckSpecVersion { + const NAME: &str = "CheckSpecVersion"; -impl TransactionExtension for CheckSpecVersion { - type Decoded = (); - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "CheckSpecVersion" + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + _v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + Ok(()) + } + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + self.0.encode_to(v); + Ok(()) } } /// The [`CheckNonce`] transaction extension. pub struct CheckNonce(u64); -impl ExtrinsicParams for CheckNonce { +impl TransactionExtension for CheckNonce { + type Decoded = u64; type Params = CheckNonceParams; - fn new(_client: &ClientState, params: Self::Params) -> Result { + fn new( + _client: &ClientState, + params: Self::Params, + ) -> Result { Ok(CheckNonce(params.0.unwrap_or(0))) } } -impl ExtrinsicParamsEncoder for CheckNonce { - fn encode_value_to(&self, v: &mut Vec) { +impl frame_decode::extrinsics::TransactionExtension for CheckNonce { + const NAME: &str = "CheckNonce"; + + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { Compact(self.0).encode_to(v); + Ok(()) } -} - -impl TransactionExtension for CheckNonce { - type Decoded = u64; - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "CheckNonce" + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + _v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + Ok(()) } } @@ -236,48 +271,76 @@ impl Params for CheckNonceParams { /// The [`CheckTxVersion`] transaction extension. pub struct CheckTxVersion(u32); -impl ExtrinsicParams for CheckTxVersion { +impl TransactionExtension for CheckTxVersion { + type Decoded = (); type Params = (); - fn new(client: &ClientState, _params: Self::Params) -> Result { + fn new( + client: &ClientState, + _params: Self::Params, + ) -> Result { Ok(CheckTxVersion(client.transaction_version)) } } -impl ExtrinsicParamsEncoder for CheckTxVersion { - fn encode_implicit_to(&self, v: &mut Vec) { - self.0.encode_to(v); - } -} +impl frame_decode::extrinsics::TransactionExtension for CheckTxVersion { + const NAME: &str = "CheckTxVersion"; -impl TransactionExtension for CheckTxVersion { - type Decoded = (); - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "CheckTxVersion" + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + _v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + Ok(()) + } + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + self.0.encode_to(v); + Ok(()) } } /// The [`CheckGenesis`] transaction extension. pub struct CheckGenesis(HashFor); -impl ExtrinsicParams for CheckGenesis { +impl TransactionExtension for CheckGenesis { + type Decoded = (); type Params = (); - fn new(client: &ClientState, _params: Self::Params) -> Result { + fn new( + client: &ClientState, + _params: Self::Params, + ) -> Result { Ok(CheckGenesis(client.genesis_hash)) } } -impl ExtrinsicParamsEncoder for CheckGenesis { - fn encode_implicit_to(&self, v: &mut Vec) { - self.0.encode_to(v); - } -} +impl frame_decode::extrinsics::TransactionExtension + for CheckGenesis +{ + const NAME: &str = "CheckGenesis"; -impl TransactionExtension for CheckGenesis { - type Decoded = (); - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "CheckGenesis" + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + _v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + Ok(()) + } + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + self.0.encode_to(v); + Ok(()) } } @@ -287,15 +350,19 @@ pub struct CheckMortality { genesis_hash: HashFor, } -impl ExtrinsicParams for CheckMortality { +impl TransactionExtension for CheckMortality { + type Decoded = Era; type Params = CheckMortalityParams; - fn new(client: &ClientState, params: Self::Params) -> Result { + fn new( + client: &ClientState, + params: Self::Params, + ) -> Result { // If a user has explicitly configured the transaction to be mortal for n blocks, but we get // to this stage and no injected information was able to turn this into MortalFromBlock{..}, // then we hit an error as we are unable to construct a mortal transaction here. if matches!(¶ms.0, CheckMortalityParamsInner::MortalForBlocks(_)) { - return Err(ExtrinsicParamsError::custom( + return Err(TransactionExtensionError::custom( "CheckMortality: We cannot construct an offline extrinsic with only the number of blocks it is mortal for. Use mortal_from_unchecked instead.", )); } @@ -309,8 +376,17 @@ impl ExtrinsicParams for CheckMortality { } } -impl ExtrinsicParamsEncoder for CheckMortality { - fn encode_value_to(&self, v: &mut Vec) { +impl frame_decode::extrinsics::TransactionExtension + for CheckMortality +{ + const NAME: &str = "CheckMortality"; + + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { match &self.params { CheckMortalityParamsInner::MortalFromBlock { for_n_blocks, @@ -326,8 +402,14 @@ impl ExtrinsicParamsEncoder for CheckMortality { Era::Immortal.encode_to(v); } } - } - fn encode_implicit_to(&self, v: &mut Vec) { + Ok(()) + } + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { match &self.params { CheckMortalityParamsInner::MortalFromBlock { from_block_hash, .. @@ -338,13 +420,7 @@ impl ExtrinsicParamsEncoder for CheckMortality { self.genesis_hash.encode_to(v); } } - } -} - -impl TransactionExtension for CheckMortality { - type Decoded = Era; - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "CheckMortality" + Ok(()) } } @@ -440,10 +516,14 @@ impl ChargeAssetTxPayment { } } -impl ExtrinsicParams for ChargeAssetTxPayment { +impl TransactionExtension for ChargeAssetTxPayment { + type Decoded = Self; type Params = ChargeAssetTxPaymentParams; - fn new(_client: &ClientState, params: Self::Params) -> Result { + fn new( + _client: &ClientState, + params: Self::Params, + ) -> Result { Ok(ChargeAssetTxPayment { tip: Compact(params.tip), asset_id: params.asset_id, @@ -451,16 +531,27 @@ impl ExtrinsicParams for ChargeAssetTxPayment { } } -impl ExtrinsicParamsEncoder for ChargeAssetTxPayment { - fn encode_value_to(&self, v: &mut Vec) { +impl frame_decode::extrinsics::TransactionExtension + for ChargeAssetTxPayment +{ + const NAME: &str = "ChargeAssetTxPayment"; + + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { (self.tip, &self.asset_id).encode_to(v); + Ok(()) } -} - -impl TransactionExtension for ChargeAssetTxPayment { - type Decoded = Self; - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "ChargeAssetTxPayment" + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + _v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + Ok(()) } } @@ -520,26 +611,39 @@ impl ChargeTransactionPayment { } } -impl ExtrinsicParams for ChargeTransactionPayment { +impl TransactionExtension for ChargeTransactionPayment { + type Decoded = Self; type Params = ChargeTransactionPaymentParams; - fn new(_client: &ClientState, params: Self::Params) -> Result { + 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) { +impl frame_decode::extrinsics::TransactionExtension for ChargeTransactionPayment { + const NAME: &str = "ChargeTransactionPayment"; + + fn encode_value_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { self.tip.encode_to(v); + Ok(()) } -} - -impl TransactionExtension for ChargeTransactionPayment { - type Decoded = Self; - fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool { - identifier == "ChargeTransactionPayment" + fn encode_implicit_to( + &self, + _type_id: u32, + _type_resolver: &PortableRegistry, + _v: &mut Vec, + ) -> Result<(), frame_decode::extrinsics::TransactionExtensionError> { + Ok(()) } } @@ -562,145 +666,3 @@ impl ChargeTransactionPaymentParams { } impl Params for ChargeTransactionPaymentParams {} - -/// 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 { - params: Vec>, - _marker: core::marker::PhantomData<(T, Params)>, -} - -macro_rules! impl_tuples { - ($($ident:ident $index:tt),+) => { - // We do some magic when the tuple is wrapped in AnyOf. We - // look at the metadata, and use this to select and make use of only the extensions - // that we actually need for the chain we're dealing with. - impl ExtrinsicParams for AnyOf - where - T: Config, - $($ident: TransactionExtension,)+ - { - type Params = ($($ident::Params,)+); - - fn new( - client: &ClientState, - params: Self::Params, - ) -> Result { - let metadata = &client.metadata; - let types = metadata.types(); - - // 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().transaction_extensions_to_use_for_encoding().enumerate() { - // Skip over any exts that have a match already: - if exts_by_index.contains_key(&idx) { - continue - } - // Break and record as soon as we find a match: - if $ident::matches(e.identifier(), e.extra_ty(), types) { - let ext = $ident::new(client, params.$index)?; - let boxed_ext: Box = Box::new(ext); - exts_by_index.insert(idx, boxed_ext); - break - } - } - })+ - - // 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().transaction_extensions_to_use_for_encoding().enumerate() { - let Some(ext) = exts_by_index.remove(&idx) else { - if is_type_empty(e.extra_ty(), types) { - continue - } else { - return Err(ExtrinsicParamsError::UnknownTransactionExtension(e.identifier().to_owned())); - } - }; - params.push(ext); - } - - Ok(AnyOf { - params, - _marker: core::marker::PhantomData - }) - } - } - - impl ExtrinsicParamsEncoder for AnyOf - where - T: Config, - $($ident: TransactionExtension,)+ - { - fn encode_value_to(&self, v: &mut Vec) { - for ext in &self.params { - ext.encode_value_to(v); - } - } - fn encode_signer_payload_value_to(&self, v: &mut Vec) { - for ext in &self.params { - 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); - } - } - } - } -} - -#[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); -}; - -/// Checks to see whether the type being given is empty, ie would require -/// 0 bytes to encode. -fn is_type_empty(type_id: u32, types: &scale_info::PortableRegistry) -> bool { - let Some(ty) = types.resolve(type_id) else { - // Can't resolve; type may not be empty. Not expected to hit this. - return false; - }; - - use scale_info::TypeDef; - match &ty.type_def { - TypeDef::Composite(c) => c.fields.iter().all(|f| is_type_empty(f.ty.id, types)), - TypeDef::Array(a) => a.len == 0 || is_type_empty(a.type_param.id, types), - TypeDef::Tuple(t) => t.fields.iter().all(|f| is_type_empty(f.id, types)), - // Explicitly list these in case any additions are made in the future. - TypeDef::BitSequence(_) - | TypeDef::Variant(_) - | TypeDef::Sequence(_) - | TypeDef::Compact(_) - | TypeDef::Primitive(_) => false, - } -} diff --git a/subxt/src/error.rs b/subxt/src/error.rs index 16aaf0b53e..0094548309 100644 --- a/subxt/src/error.rs +++ b/subxt/src/error.rs @@ -215,34 +215,18 @@ impl OfflineClientAtBlockError { #[non_exhaustive] #[allow(missing_docs)] pub enum OnlineClientError { - #[error("Cannot construct OnlineClient: The URL provided is invalid: {url}")] - InvalidUrl { - /// The URL that was invalid. - url: String, - }, #[error("Cannot construct OnlineClient: {0}")] RpcError(#[from] subxt_rpcs::Error), #[error("Could not construct the CombinedBackend: {0}")] CannotBuildCombinedBackend(CombinedBackendError), - #[error( - "Cannot construct OnlineClient: Cannot fetch latest finalized block to obtain init details from: {0}" - )] - CannotGetLatestFinalizedBlock(BackendError), #[error("Cannot construct OnlineClient: Cannot fetch genesis hash: {0}")] CannotGetGenesisHash(BackendError), - #[error("Cannot construct OnlineClient: Cannot fetch current runtime version: {0}")] - CannotGetCurrentRuntimeVersion(BackendError), - #[error("Cannot construct OnlineClient: Cannot fetch metadata: {0}")] - CannotFetchMetadata(BackendError), } impl OnlineClientError { fn backend_error(&self) -> Option<&BackendError> { match self { - OnlineClientError::CannotGetLatestFinalizedBlock(e) - | OnlineClientError::CannotGetGenesisHash(e) - | OnlineClientError::CannotGetCurrentRuntimeVersion(e) - | OnlineClientError::CannotFetchMetadata(e) => Some(e), + OnlineClientError::CannotGetGenesisHash(e) => Some(e), _ => None, } } @@ -422,12 +406,6 @@ impl BlockError { pub enum BackendError { #[error("Backend error: RPC error: {0}")] Rpc(#[from] RpcError), - #[error("Backend error: Could not find metadata version {0}")] - MetadataVersionNotFound(u32), - #[error("Backend error: Could not codec::Decode Runtime API response: {0}")] - CouldNotScaleDecodeRuntimeResponse(codec::Error), - #[error("Backend error: Could not codec::Decode metadata bytes into subxt::Metadata: {0}")] - CouldNotDecodeMetadata(codec::Error), // This is for errors in `Backend` implementations which aren't any of the "pre-defined" set above: #[error("Custom backend error: {0}")] Other(Cow<'static, str>), @@ -497,9 +475,9 @@ pub enum RpcError { #[allow(missing_docs)] pub enum AccountNonceError { #[error("Could not retrieve account nonce: {0}")] - CouldNotRetrieve(#[from] BackendError), + CouldNotRetrieve(BackendError), #[error("Could not decode account nonce: {0}")] - CouldNotDecode(#[from] codec::Error), + CouldNotDecode(codec::Error), #[error("Wrong number of account nonce bytes returned: {0} (expected 2, 4 or 8)")] WrongNumberOfBytes(usize), } @@ -531,8 +509,6 @@ pub enum RuntimeApiError { CouldNotEncodeInputs(frame_decode::runtime_apis::RuntimeApiInputsEncodeError), #[error("Failed to decode Runtime API: {0}")] CouldNotDecodeResponse(frame_decode::runtime_apis::RuntimeApiDecodeError), - #[error("Cannot access Runtime APIs at latest block: Cannot fetch latest finalized block: {0}")] - CannotGetLatestFinalizedBlock(BackendError), #[error("Cannot call the Runtime API: {0}")] CannotCallApi(BackendError), } @@ -540,8 +516,7 @@ pub enum RuntimeApiError { impl RuntimeApiError { fn backend_error(&self) -> Option<&BackendError> { match self { - RuntimeApiError::CannotGetLatestFinalizedBlock(e) - | RuntimeApiError::CannotCallApi(e) => Some(e), + RuntimeApiError::CannotCallApi(e) => Some(e), _ => None, } } @@ -588,8 +563,6 @@ pub enum EventsError { event_name: String, reason: scale_decode::Error, }, - #[error("Cannot access events at latest block: Cannot fetch latest finalized block: {0}")] - CannotGetLatestFinalizedBlock(BackendError), #[error("Cannot fetch event bytes: {0}")] CannotFetchEventBytes(BackendError), } @@ -597,8 +570,7 @@ pub enum EventsError { impl EventsError { fn backend_error(&self) -> Option<&BackendError> { match self { - EventsError::CannotGetLatestFinalizedBlock(e) - | EventsError::CannotFetchEventBytes(e) => Some(e), + EventsError::CannotFetchEventBytes(e) => Some(e), _ => None, } } @@ -609,6 +581,8 @@ impl EventsError { #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum ExtrinsicError { + #[error("Failed to construct extrinsic: {0}")] + EncodeError(#[from] frame_decode::extrinsics::ExtrinsicEncodeError), #[error("The extrinsic payload is not compatible with the live chain")] IncompatibleCodegen, #[error("Can't find extrinsic: pallet with name {0} not found")] @@ -618,14 +592,12 @@ pub enum ExtrinsicError { pallet_name: String, call_name: String, }, - #[error("Can't encode the extrinsic call data: {0}")] - CannotEncodeCallData(scale_encode::Error), #[error("Failed to encode an extrinsic: the genesis hash was not provided")] GenesisHashNotProvided, #[error("Subxt does not support the extrinsic versions expected by the chain")] UnsupportedVersion, #[error("Cannot construct the required transaction extensions: {0}")] - Params(#[from] ExtrinsicParamsError), + Params(#[from] TransactionExtensionError), #[error("Cannot decode transaction extension '{name}': {error}")] CouldNotDecodeTransactionExtension { /// The extension name. @@ -633,17 +605,6 @@ pub enum ExtrinsicError { /// The decode error. error: scale_decode::Error, }, - #[error( - "After decoding the extrinsic at index {extrinsic_index}, {num_leftover_bytes} bytes were left, suggesting that decoding may have failed" - )] - LeftoverBytes { - /// Index of the extrinsic that failed to decode. - extrinsic_index: usize, - /// Number of bytes leftover after decoding the extrinsic. - num_leftover_bytes: usize, - }, - #[error("{0}")] - ExtrinsicDecodeErrorAt(#[from] ExtrinsicDecodeErrorAt), #[error("Failed to decode the fields of an extrinsic at index {extrinsic_index}: {error}")] CannotDecodeFields { /// Index of the extrinsic whose fields we could not decode @@ -662,12 +623,6 @@ pub enum ExtrinsicError { CannotGetBlockBody(BackendError), #[error("Block not found: {0}")] BlockNotFound(Hex), - #[error( - "Extrinsic submission error: Cannot get latest finalized block to grab account nonce at: {0}" - )] - CannotGetLatestFinalizedBlock(BackendError), - #[error("Cannot find block header for block {block_hash}")] - CannotFindBlockHeader { block_hash: Hex }, #[error("Error getting account nonce at block {block_hash}")] AccountNonceError { block_hash: Hex, @@ -702,7 +657,6 @@ impl ExtrinsicError { fn backend_error(&self) -> Option<&BackendError> { match self { ExtrinsicError::CannotGetBlockBody(e) - | ExtrinsicError::CannotGetLatestFinalizedBlock(e) | ExtrinsicError::ErrorSubmittingTransaction(e) | ExtrinsicError::TransactionStatusStreamError(e) | ExtrinsicError::CannotGetFeeInfo(e) @@ -749,10 +703,6 @@ pub enum ViewFunctionError { CouldNotEncodeInputs(frame_decode::view_functions::ViewFunctionInputsEncodeError), #[error("Failed to decode View Function: {0}")] CouldNotDecodeResponse(frame_decode::view_functions::ViewFunctionDecodeError), - #[error( - "Cannot access View Functions at latest block: Cannot fetch latest finalized block: {0}" - )] - CannotGetLatestFinalizedBlock(BackendError), #[error("Cannot call the View Function Runtime API: {0}")] CannotCallApi(BackendError), } @@ -760,8 +710,7 @@ pub enum ViewFunctionError { impl ViewFunctionError { fn backend_error(&self) -> Option<&BackendError> { match self { - ViewFunctionError::CannotGetLatestFinalizedBlock(e) - | ViewFunctionError::CannotCallApi(e) => Some(e), + ViewFunctionError::CannotCallApi(e) => Some(e), _ => None, } } @@ -987,8 +936,6 @@ pub enum StorageError { "Wrong number of key parts provided to fetch a storage address. We expected {expected} key parts but got {got} key parts" )] WrongNumberOfKeyPartsProvidedForFetching { expected: usize, got: usize }, - #[error("Cannot access storage at latest block: Cannot fetch latest finalized block: {0}")] - CannotGetLatestFinalizedBlock(BackendError), #[error( "No storage value found at the given address, and no default value to fall back to using." )] @@ -1006,8 +953,7 @@ pub enum StorageError { impl StorageError { fn backend_error(&self) -> Option<&BackendError> { match self { - StorageError::CannotGetLatestFinalizedBlock(e) - | StorageError::CannotFetchValue(e) + StorageError::CannotFetchValue(e) | StorageError::CannotIterateValues(e) | StorageError::StreamFailure(e) => Some(e), _ => None, @@ -1114,35 +1060,26 @@ pub enum ExtrinsicDecodeErrorAtReason { LeftoverBytes(Vec), } -/// An error that can be emitted when trying to construct an instance of [`crate::config::ExtrinsicParams`], +/// An error that can be emitted when trying to construct a [`crate::config::TransactionExtension`], /// encode data from the instance, or match on signed extensions. #[derive(Debug, DeriveError)] #[non_exhaustive] #[allow(missing_docs)] -pub enum ExtrinsicParamsError { - #[error("Cannot find type id '{type_id} in the metadata (context: {context})")] - MissingTypeId { - /// Type ID. - type_id: u32, - /// Some arbitrary context to help narrow the source of the error. - context: &'static str, - }, - #[error("The chain expects a signed extension with the name {0}, but we did not provide one")] - UnknownTransactionExtension(String), +pub enum TransactionExtensionError { #[error("Error constructing extrinsic parameters: {0}")] Custom(Box), } -impl ExtrinsicParamsError { - /// Create a custom [`ExtrinsicParamsError`] from a string. +impl TransactionExtensionError { + /// Create a custom [`TransactionExtensionError`] from a string. pub fn custom>(error: S) -> Self { let error: String = error.into(); let error: Box = Box::from(error); - ExtrinsicParamsError::Custom(error) + TransactionExtensionError::Custom(error) } } -impl From for ExtrinsicParamsError { +impl From for TransactionExtensionError { fn from(value: core::convert::Infallible) -> Self { match value {} } diff --git a/subxt/src/extrinsics/extrinsic_transaction_extensions.rs b/subxt/src/extrinsics/extrinsic_transaction_extensions.rs index 65092e68cf..24667d2d4e 100644 --- a/subxt/src/extrinsics/extrinsic_transaction_extensions.rs +++ b/subxt/src/extrinsics/extrinsic_transaction_extensions.rs @@ -90,26 +90,26 @@ pub struct ExtrinsicTransactionExtension<'extrinsic, T: Config> { } impl<'extrinsic, T: Config> ExtrinsicTransactionExtension<'extrinsic, T> { - /// The bytes representing this signed extension. + /// The bytes representing this transaction extension. pub fn bytes(&self) -> &'extrinsic [u8] { self.bytes } - /// The name of the signed extension. + /// The name of the transaction extension. pub fn name(&self) -> &'extrinsic str { self.identifier } - /// The type id of the signed extension. + /// The type id of the transaction extension. pub fn type_id(&self) -> u32 { self.ty_id } - /// Decodes this signed extension based on the provided [`TransactionExtension`] type. + /// Decodes this transaction extension based on the provided [`TransactionExtension`] type. pub fn decode_as>( &self, ) -> Option> { - if !S::matches(self.identifier, self.ty_id, self.metadata.types()) { + if S::NAME != self.identifier { return None; } Some(self.decode_unchecked_as::()) diff --git a/subxt/src/transactions.rs b/subxt/src/transactions.rs index bbd7a18547..0e328d3dc1 100644 --- a/subxt/src/transactions.rs +++ b/subxt/src/transactions.rs @@ -11,16 +11,11 @@ mod validation_result; use crate::backend::TransactionStatus as BackendTransactionStatus; use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT}; -use crate::config::extrinsic_params::Params; -use crate::config::{ - ClientState, Config, ExtrinsicParams, ExtrinsicParamsEncoder, HashFor, Hasher, Header, -}; +use crate::config::transaction_extensions::Params; +use crate::config::{ClientState, Config, HashFor, Hasher, TransactionExtensions}; use crate::error::{ExtrinsicError, TransactionStatusError}; use codec::{Compact, Decode, Encode}; use core::marker::PhantomData; -use futures::{TryFutureExt, future::try_join}; -use sp_crypto_hashing::blake2_256; -use std::borrow::Cow; pub use default_params::DefaultParams; pub use payload::{DynamicPayload, Payload, StaticPayload, dynamic}; @@ -96,10 +91,15 @@ impl> TransactionsClient where Call: Payload, { - let mut bytes = Vec::new(); - let metadata = self.client.metadata_ref(); - call.encode_call_data_to(metadata, &mut bytes)?; - Ok(bytes) + let encoded = frame_decode::extrinsics::encode_call_data( + call.pallet_name(), + call.call_name(), + call.call_data(), + self.client.metadata_ref(), + self.client.metadata_ref().types(), + )?; + + Ok(encoded) } /// Creates an unsigned transaction without submitting it. Depending on the metadata, we might end @@ -128,7 +128,21 @@ impl> TransactionsClient where Call: Payload, { - self.create_unsigned_at_version(call, SupportedTransactionVersion::V4) + self.validate(call)?; + + let encoded = frame_decode::extrinsics::encode_v4_unsigned( + call.pallet_name(), + call.call_name(), + call.call_data(), + self.client.metadata_ref(), + self.client.metadata_ref().types(), + )?; + + Ok(SubmittableTransaction { + client: self.client.clone(), + encoded, + marker: PhantomData, + }) } /// Creates a V5 "bare" transaction without submitting it. @@ -139,7 +153,21 @@ impl> TransactionsClient where Call: Payload, { - self.create_unsigned_at_version(call, SupportedTransactionVersion::V5) + self.validate(call)?; + + let encoded = frame_decode::extrinsics::encode_v5_bare( + call.pallet_name(), + call.call_name(), + call.call_data(), + self.client.metadata_ref(), + self.client.metadata_ref().types(), + )?; + + Ok(SubmittableTransaction { + client: self.client.clone(), + encoded, + marker: PhantomData, + }) } /// Create a signable transaction. Depending on the metadata, we might end up constructing either a v4 or @@ -148,11 +176,11 @@ impl> TransactionsClient /// /// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_. /// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values. - pub fn create_signable_offline( + pub fn create_signable_offline<'call, Call>( &self, - call: &Call, - params: >::Params, - ) -> Result, ExtrinsicError> + call: &'call Call, + params: >::Params, + ) -> Result, ExtrinsicError> where Call: Payload, { @@ -169,11 +197,11 @@ impl> TransactionsClient /// /// Prefer [`Self::create_signable_offline()`] if you don't know which version to create; this will pick the /// most suitable one for the given chain. - pub fn create_v4_signable_offline( + pub fn create_v4_signable_offline<'call, Call>( &self, - call: &Call, - params: >::Params, - ) -> Result, ExtrinsicError> + call: &'call Call, + params: >::Params, + ) -> Result, ExtrinsicError> where Call: Payload, { @@ -187,11 +215,11 @@ impl> TransactionsClient /// /// Prefer [`Self::create_signable_offline()`] if you don't know which version to create; this will pick the /// most suitable one for the given chain. - pub fn create_v5_signable_offline( + pub fn create_v5_signable_offline<'call, Call>( &self, - call: &Call, - params: >::Params, - ) -> Result, ExtrinsicError> + call: &'call Call, + params: >::Params, + ) -> Result, ExtrinsicError> where Call: Payload, { @@ -218,50 +246,13 @@ impl> TransactionsClient } } - // Create a V4 "unsigned" transaction or V5 "bare" transaction. - fn create_unsigned_at_version( - &self, - call: &Call, - tx_version: SupportedTransactionVersion, - ) -> Result, ExtrinsicError> { - let metadata = self.client.metadata_ref(); - - // 1. Validate this call against the current node metadata if the call comes - // with a hash allowing us to do so. - self.validate(call)?; - - // 2. Encode extrinsic - let extrinsic = { - let mut encoded_inner = Vec::new(); - // encode the transaction version first. - (tx_version as u8).encode_to(&mut encoded_inner); - // encode call data after this byte. - call.encode_call_data_to(metadata, &mut encoded_inner)?; - // now, prefix byte length: - let len = Compact( - u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"), - ); - let mut encoded = Vec::new(); - len.encode_to(&mut encoded); - encoded.extend(encoded_inner); - encoded - }; - - // Wrap in Encoded to ensure that any more "encode" calls leave it in the right state. - Ok(SubmittableTransaction { - client: self.client.clone(), - encoded: extrinsic, - marker: PhantomData, - }) - } - // Create a V4 "signed" or a V5 "general" transaction. - fn create_signable_at_version( + fn create_signable_at_version<'call, Call>( &self, - call: &Call, - params: >::Params, + call: &'call Call, + params: >::Params, tx_version: SupportedTransactionVersion, - ) -> Result, ExtrinsicError> + ) -> Result, ExtrinsicError> where Call: Payload, { @@ -282,9 +273,6 @@ impl> TransactionsClient } }; - // 3. SCALE encode call data to bytes (pallet u8, call u8, call params). - let call_data = self.call_data(call)?; - // 4. Construct our custom additional/extra params. let client_state = ClientState { genesis_hash: self @@ -295,14 +283,14 @@ impl> TransactionsClient transaction_version: self.client.transaction_version(), metadata: self.client.metadata(), }; - let additional_and_extra_params = - >::new(&client_state, params)?; + let tx_extensions = + >::new(&client_state, params)?; // Return these details, ready to construct a signed extrinsic from. Ok(SignableTransaction { client: self.client.clone(), - call_data, - additional_and_extra_params, + call, + tx_extensions, tx_extension_version, }) } @@ -321,12 +309,12 @@ impl> TransactionsClient { } /// Creates a signable transaction. This can then be signed and submitted. - pub async fn create_signable( + pub async fn create_signable<'call, Call>( &self, - call: &Call, + call: &'call Call, account_id: &T::AccountId, - mut params: >::Params, - ) -> Result, ExtrinsicError> + mut params: >::Params, + ) -> Result, ExtrinsicError> where Call: Payload, { @@ -339,12 +327,12 @@ impl> TransactionsClient { /// /// Prefer [`Self::create_signable()`] if you don't know which version to create; this will pick the /// most suitable one for the given chain. - pub async fn create_v4_signable( + pub async fn create_v4_signable<'call, Call>( &self, - call: &Call, + call: &'call Call, account_id: &T::AccountId, - mut params: >::Params, - ) -> Result, ExtrinsicError> + mut params: >::Params, + ) -> Result, ExtrinsicError> where Call: Payload, { @@ -357,12 +345,12 @@ impl> TransactionsClient { /// /// Prefer [`Self::create_signable()`] if you don't know which version to create; this will pick the /// most suitable one for the given chain. - pub async fn create_v5_signable( + pub async fn create_v5_signable<'call, Call>( &self, - call: &Call, + call: &'call Call, account_id: &T::AccountId, - mut params: >::Params, - ) -> Result, ExtrinsicError> + mut params: >::Params, + ) -> Result, ExtrinsicError> where Call: Payload, { @@ -376,7 +364,7 @@ impl> TransactionsClient { &mut self, call: &Call, signer: &S, - params: >::Params, + params: >::Params, ) -> Result, ExtrinsicError> where Call: Payload, @@ -386,7 +374,7 @@ impl> TransactionsClient { .create_signable(call, &signer.account_id(), params) .await?; - Ok(signable.sign(signer)) + signable.sign(signer) } /// Creates and signs an transaction and submits it to the chain. Passes default parameters @@ -402,7 +390,7 @@ impl> TransactionsClient { where Call: Payload, S: Signer, - >::Params: DefaultParams, + >::Params: DefaultParams, { self.sign_and_submit_then_watch(call, signer, DefaultParams::default_params()) .await @@ -416,7 +404,7 @@ impl> TransactionsClient { &mut self, call: &Call, signer: &S, - params: >::Params, + params: >::Params, ) -> Result, ExtrinsicError> where Call: Payload, @@ -446,7 +434,7 @@ impl> TransactionsClient { where Call: Payload, S: Signer, - >::Params: DefaultParams, + >::Params: DefaultParams, { self.sign_and_submit(call, signer, DefaultParams::default_params()) .await @@ -464,7 +452,7 @@ impl> TransactionsClient { &mut self, call: &Call, signer: &S, - params: >::Params, + params: >::Params, ) -> Result, ExtrinsicError> where Call: Payload, @@ -476,34 +464,19 @@ impl> TransactionsClient { .await } - /// Fetch the latest block header and account nonce from the backend and use them to refine [`ExtrinsicParams::Params`]. + /// Fetch the block header and account nonce from the current block and use + /// them to refine our [`TransactionExtensions::Params`]. async fn inject_account_nonce_and_block( &self, account_id: &T::AccountId, - params: &mut >::Params, + params: &mut >::Params, ) -> Result<(), ExtrinsicError> { - let block_ref = self - .client - .backend() - .latest_finalized_block_ref() - .await - .map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?; - - let (block_header, account_nonce) = try_join( - self.client - .backend() - .block_header(block_ref.hash()) - .map_err(ExtrinsicError::CannotGetLatestFinalizedBlock), - self.account_nonce(account_id), - ) - .await?; - - let block_header = block_header.ok_or_else(|| ExtrinsicError::CannotFindBlockHeader { - block_hash: block_ref.hash().into(), - })?; + let block_number = self.client.block_number(); + let block_hash = self.client.block_ref().hash(); + let account_nonce = self.account_nonce(account_id).await?; params.inject_account_nonce(account_nonce); - params.inject_block(block_header.number(), block_ref.hash()); + params.inject_block(block_number, block_hash); Ok(()) } @@ -520,33 +493,55 @@ pub enum SupportedTransactionVersion { } /// This is a transaction that requires signing before it can be submitted. -pub struct SignableTransaction { +pub struct SignableTransaction<'call, T: Config, Client, Call> { client: Client, - call_data: Vec, - additional_and_extra_params: ::ExtrinsicParams, + call: &'call Call, + tx_extensions: ::TransactionExtensions, // For V4 transactions this doesn't exist, and for V5 it does. tx_extension_version: Option, } -impl> SignableTransaction { - /// Return the bytes representing the call data for this partially constructed - /// transaction. - pub fn call_data(&self) -> &[u8] { - &self.call_data - } - +impl<'call, T: Config, Client: OfflineClientAtBlockT, Call: Payload> + SignableTransaction<'call, T, Client, Call> +{ /// Return the signer payload for this transaction. These are the bytes that must /// be signed in order to produce a valid signature for the transaction. - pub fn signer_payload(&self) -> Vec { - self.with_signer_payload(|bytes| bytes.to_vec()) + pub fn signer_payload(&self) -> Result, ExtrinsicError> { + let signer_payload = if let Some(transaction_extension_version) = self.tx_extension_version + { + frame_decode::extrinsics::encode_v5_signer_payload( + self.call.pallet_name(), + self.call.call_name(), + self.call.call_data(), + transaction_extension_version, + &self.tx_extensions, + self.client.metadata_ref(), + self.client.metadata_ref().types(), + )? + .to_vec() + } else { + frame_decode::extrinsics::encode_v4_signer_payload( + self.call.pallet_name(), + self.call.call_name(), + self.call.call_data(), + &self.tx_extensions, + self.client.metadata_ref(), + self.client.metadata_ref().types(), + )? + }; + + Ok(signer_payload) } /// Convert this [`SignableTransaction`] into a [`SubmittableTransaction`], ready to submit. /// The provided `signer` is responsible for providing the "from" address for the transaction, /// as well as providing a signature to attach to it. - pub fn sign>(&mut self, signer: &S) -> SubmittableTransaction { + pub fn sign>( + &mut self, + signer: &S, + ) -> Result, ExtrinsicError> { // Given our signer, we can sign the payload representing this extrinsic. - let signature = signer.sign(&self.signer_payload()); + 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) } @@ -559,84 +554,41 @@ impl> SignableTransaction &mut self, account_id: &T::AccountId, signature: &T::Signature, - ) -> SubmittableTransaction { - let encoded = if let Some(tx_extensions_version) = self.tx_extension_version { - let mut encoded_inner = Vec::new(); + ) -> Result, ExtrinsicError> { + let encoded = if let Some(transaction_extension_version) = self.tx_extension_version { // Pass account and signature to extensions to be added. - self.additional_and_extra_params - .inject_signature(account_id, signature); - // "is general" + transaction protocol version (5) - (0b01000000 + 5u8).encode_to(&mut encoded_inner); - // Encode versions for the transaction extensions - tx_extensions_version.encode_to(&mut encoded_inner); - // Encode the actual transaction extensions values - self.additional_and_extra_params - .encode_value_to(&mut encoded_inner); - // and now, call data (remembering that it's been encoded already and just needs appending) - encoded_inner.extend(&self.call_data); - // now, prefix byte length: - let len = Compact( - u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"), - ); - let mut encoded = Vec::new(); - len.encode_to(&mut encoded); - encoded.extend(encoded_inner); - encoded + self.tx_extensions.inject_signature(account_id, signature); + + frame_decode::extrinsics::encode_v5_general( + self.call.pallet_name(), + self.call.call_name(), + self.call.call_data(), + transaction_extension_version, + &self.tx_extensions, + self.client.metadata_ref(), + self.client.metadata_ref().types(), + ) } else { - let mut encoded_inner = Vec::new(); - // "is signed" + transaction protocol version (4) - (0b10000000 + 4u8).encode_to(&mut encoded_inner); - // from address for signature + // We need an Address, not an AccountId, to create a tx. let address: T::Address = account_id.clone().into(); - address.encode_to(&mut encoded_inner); - // the signature - signature.encode_to(&mut encoded_inner); - // attach custom extra params - self.additional_and_extra_params - .encode_value_to(&mut encoded_inner); - // and now, call data (remembering that it's been encoded already and just needs appending) - encoded_inner.extend(&self.call_data); - // now, prefix byte length: - let len = Compact( - u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"), - ); - let mut encoded = Vec::new(); - len.encode_to(&mut encoded); - encoded.extend(encoded_inner); - encoded - }; - SubmittableTransaction { + frame_decode::extrinsics::encode_v4_signed( + self.call.pallet_name(), + self.call.call_name(), + self.call.call_data(), + &self.tx_extensions, + &address, + &signature, + self.client.metadata_ref(), + self.client.metadata_ref().types(), + ) + }?; + + Ok(SubmittableTransaction { client: self.client.clone(), encoded, marker: PhantomData, - } - } - - // Obtain bytes representing the signer payload and run call some function - // with them. This can avoid an allocation in some cases. - fn with_signer_payload(&self, f: F) -> R - where - F: for<'a> FnOnce(Cow<'a, [u8]>) -> R, - { - let mut bytes = self.call_data.clone(); - self.additional_and_extra_params - .encode_signer_payload_value_to(&mut bytes); - self.additional_and_extra_params - .encode_implicit_to(&mut bytes); - - // For V5 transactions we _always_ blake2 hash. For V4 we only - // hash if more than 256 bytes in the payload. - if self.is_v5() || bytes.len() > 256 { - f(Cow::Borrowed(&blake2_256(&bytes))) - } else { - f(Cow::Owned(bytes)) - } - } - - // Are we working with a V5 transaction? This is handled a bit differently. - fn is_v5(&self) -> bool { - self.tx_extension_version.is_some() + }) } } @@ -758,17 +710,15 @@ impl> SubmittableTransaction Result { + let block_hash = self.client.block_ref().hash(); + + // Params for the Runtime API call let mut params = self.encoded().to_vec(); (self.encoded().len() as u32).encode_to(&mut params); - let latest_block_ref = self - .client - .backend() - .latest_finalized_block_ref() - .await - .map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?; // destructuring RuntimeDispatchInfo, see type information // data layout: {weight_ref_time: Compact, weight_proof_size: Compact, class: u8, partial_fee: u128} @@ -778,7 +728,7 @@ impl> SubmittableTransaction u16::decode(cursor)?.into(), - 4 => u32::decode(cursor)?.into(), - 8 => u64::decode(cursor)?, + 2 => u16::decode(cursor) + .map_err(AccountNonceError::CouldNotDecode)? + .into(), + 4 => u32::decode(cursor) + .map_err(AccountNonceError::CouldNotDecode)? + .into(), + 8 => u64::decode(cursor).map_err(AccountNonceError::CouldNotDecode)?, _ => { return Err(AccountNonceError::WrongNumberOfBytes( account_nonce_bytes.len(), diff --git a/subxt/src/transactions/payload.rs b/subxt/src/transactions/payload.rs index 66b565390a..01f2fc6cc0 100644 --- a/subxt/src/transactions/payload.rs +++ b/subxt/src/transactions/payload.rs @@ -5,30 +5,24 @@ //! This module contains the trait and types used to represent //! transactions that can be submitted. -use crate::error::ExtrinsicError; -use codec::Encode; use scale_encode::EncodeAsFields; use scale_value::{Composite, Value, ValueDef, Variant}; use std::borrow::Cow; -use subxt_metadata::Metadata; /// This represents a transaction payload that can be submitted /// to a node. pub trait Payload { - /// Encode call data to the provided output. - fn encode_call_data_to( - &self, - metadata: &Metadata, - out: &mut Vec, - ) -> Result<(), ExtrinsicError>; + /// The call data + type CallData: EncodeAsFields; - /// Encode call data and return the output. This is a convenience - /// wrapper around [`Payload::encode_call_data_to`]. - fn encode_call_data(&self, metadata: &Metadata) -> Result, ExtrinsicError> { - let mut v = Vec::new(); - self.encode_call_data_to(metadata, &mut v)?; - Ok(v) - } + /// The pallet name + fn pallet_name(&self) -> &str; + + /// The call name + fn call_name(&self) -> &str; + + /// The call data + fn call_data(&self) -> &Self::CallData; /// Returns the details needed to validate the call, which /// include a statically generated hash, the pallet name, @@ -41,15 +35,16 @@ pub trait Payload { macro_rules! boxed_payload { ($ty:path) => { impl Payload for $ty { - fn encode_call_data_to( - &self, - metadata: &Metadata, - out: &mut Vec, - ) -> Result<(), ExtrinsicError> { - self.as_ref().encode_call_data_to(metadata, out) + type CallData = T::CallData; + + fn pallet_name(&self) -> &str { + self.as_ref().pallet_name() } - fn encode_call_data(&self, metadata: &Metadata) -> Result, ExtrinsicError> { - self.as_ref().encode_call_data(metadata) + fn call_name(&self) -> &str { + self.as_ref().call_name() + } + fn call_data(&self) -> &Self::CallData { + self.as_ref().call_data() } fn validation_details(&self) -> Option> { self.as_ref().validation_details() @@ -158,37 +153,19 @@ impl StaticPayload> { } } -impl Payload for StaticPayload { - fn encode_call_data_to( - &self, - metadata: &Metadata, - out: &mut Vec, - ) -> Result<(), ExtrinsicError> { - let pallet = metadata - .pallet_by_name(&self.pallet_name) - .ok_or_else(|| ExtrinsicError::PalletNameNotFound(self.pallet_name.to_string()))?; - let call = pallet - .call_variant_by_name(&self.call_name) - .ok_or_else(|| ExtrinsicError::CallNameNotFound { - pallet_name: pallet.name().to_string(), - call_name: self.call_name.to_string(), - })?; - - let pallet_index = pallet.call_index(); - let call_index = call.index; +impl Payload for StaticPayload { + type CallData = CD; - pallet_index.encode_to(out); - call_index.encode_to(out); + fn pallet_name(&self) -> &str { + &self.pallet_name + } - let mut fields = call - .fields - .iter() - .map(|f| scale_encode::Field::new(f.ty.id, f.name.as_deref())); + fn call_name(&self) -> &str { + &self.call_name + } - self.call_data - .encode_as_fields_to(&mut fields, metadata.types(), out) - .map_err(ExtrinsicError::CannotEncodeCallData)?; - Ok(()) + fn call_data(&self) -> &Self::CallData { + &self.call_data } fn validation_details(&self) -> Option> { @@ -208,62 +185,3 @@ pub fn dynamic( ) -> DynamicPayload { StaticPayload::new(pallet_name, call_name, call_data) } - -#[cfg(test)] -mod tests { - use super::*; - use codec::Decode; - use scale_value::Composite; - - fn test_metadata() -> Metadata { - let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale"); - Metadata::decode(&mut &metadata_bytes[..]).expect("Valid metadata") - } - - #[test] - fn encode_call_with_incompatible_types_returns_error() { - let metadata = test_metadata(); - - let incompatible_data = Composite::named([ - ("dest", scale_value::Value::bool(true)), // Boolean instead of MultiAddress - ("value", scale_value::Value::string("not_a_number")), // String instead of u128 - ]); - - let payload = StaticPayload::new("Balances", "transfer_allow_death", incompatible_data); - - let mut out = Vec::new(); - let result = payload.encode_call_data_to(&metadata, &mut out); - - assert!( - result.is_err(), - "Expected error when encoding with incompatible types" - ); - } - - #[test] - fn encode_call_with_valid_data_succeeds() { - let metadata = test_metadata(); - - // Create a valid payload to ensure our error handling doesn't break valid cases - // For MultiAddress, we'll use the Id variant with a 32-byte account - let valid_address = - scale_value::Value::unnamed_variant("Id", [scale_value::Value::from_bytes([0u8; 32])]); - - let valid_data = Composite::named([ - ("dest", valid_address), - ("value", scale_value::Value::u128(1000)), - ]); - - let payload = StaticPayload::new("Balances", "transfer_allow_death", valid_data); - - // This should succeed - let mut out = Vec::new(); - let result = payload.encode_call_data_to(&metadata, &mut out); - - assert!( - result.is_ok(), - "Expected success when encoding with valid data" - ); - assert!(!out.is_empty(), "Expected encoded output to be non-empty"); - } -} diff --git a/subxt/src/utils.rs b/subxt/src/utils.rs index f2fdc389f7..6a3836a710 100644 --- a/subxt/src/utils.rs +++ b/subxt/src/utils.rs @@ -4,7 +4,6 @@ //! Miscellaneous utility helpers. -mod account_id20; mod era; #[cfg(feature = "jsonrpsee")] mod fetch_chain_spec; @@ -17,11 +16,11 @@ mod wrapper_opaque; mod yesnomaybe; pub mod bits; +pub mod eth; use codec::{Compact, Decode, Encode}; use derive_where::derive_where; -pub use account_id20::AccountId20; pub use era::Era; pub use multi_address::MultiAddress; pub use multi_signature::MultiSignature; diff --git a/subxt/src/utils/account_id20.rs b/subxt/src/utils/eth.rs similarity index 90% rename from subxt/src/utils/account_id20.rs rename to subxt/src/utils/eth.rs index e02e25b502..7ac2379389 100644 --- a/subxt/src/utils/account_id20.rs +++ b/subxt/src/utils/eth.rs @@ -2,13 +2,31 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -//! `AccountId20` is a representation of Ethereum address derived from hashing the public key. +//! A couple of convenience types to use for Ethereum style chains. use codec::{Decode, Encode}; use keccak_hash::keccak; use serde::{Deserialize, Serialize}; use thiserror::Error as DeriveError; +/// Ehtereum-compatible 65 byte ecdsa signature +#[derive( + Clone, + Copy, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + Debug, + scale_encode::EncodeAsType, + scale_decode::DecodeAsType, + scale_info::TypeInfo, +)] +pub struct Signature(pub [u8; 65]); + +/// Ethereum-compatible 20 byte `AccountId`. #[derive( Clone, Copy, @@ -23,7 +41,6 @@ use thiserror::Error as DeriveError; scale_decode::DecodeAsType, scale_info::TypeInfo, )] -/// Ethereum-compatible `AccountId`. pub struct AccountId20(pub [u8; 20]); impl AsRef<[u8]> for AccountId20 { diff --git a/subxt/src/utils/multi_signature.rs b/subxt/src/utils/multi_signature.rs index db449bf7e5..f358aef7ae 100644 --- a/subxt/src/utils/multi_signature.rs +++ b/subxt/src/utils/multi_signature.rs @@ -10,7 +10,19 @@ use codec::{Decode, Encode}; /// Signature container that can store known signature types. This is a simplified version of /// `sp_runtime::MultiSignature`. To obtain more functionality, convert this into that type. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, Debug, scale_info::TypeInfo)] +#[derive( + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + scale_encode::EncodeAsType, + scale_decode::DecodeAsType, + Encode, + Decode, + Debug, + scale_info::TypeInfo, +)] pub enum MultiSignature { /// An Ed25519 signature. Ed25519([u8; 64]), 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 2e684e07ce..59d6938de8 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 @@ -282,6 +282,7 @@ async fn transactionwatch_v1_submit_and_watch() { .create_signable_offline(&payload, Default::default()) .unwrap() .sign(&dev::alice()) + .unwrap() .into_encoded(); // Test submitting it: @@ -350,7 +351,8 @@ async fn transaction_v1_broadcast() { .unwrap() .create_signable_offline(&tx_payload, Default::default()) .unwrap() - .sign(&dev::alice()); + .sign(&dev::alice()) + .unwrap(); let tx_hash = tx.hash(); let tx_bytes = tx.into_encoded(); @@ -425,6 +427,7 @@ async fn transaction_v1_stop() { .create_signable_offline(&tx, Default::default()) .unwrap() .sign(&dev::alice()) + .unwrap() .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 1cac9308d7..ccc0000089 100644 --- a/testing/integration-tests/src/full_client/client/mod.rs +++ b/testing/integration-tests/src/full_client/client/mod.rs @@ -173,12 +173,13 @@ async fn external_signing() { .unwrap(); // Get the signer payload. - let signer_payload = partial_extrinsic.signer_payload(); + let signer_payload = partial_extrinsic.signer_payload().unwrap(); // Sign it (possibly externally). let signature = alice.sign(&signer_payload); // Use this to build a signed extrinsic. let extrinsic = partial_extrinsic - .sign_with_account_and_signature(&alice.public_key().into(), &signature.into()); + .sign_with_account_and_signature(&alice.public_key().into(), &signature.into()) + .unwrap(); // And now submit it. extrinsic diff --git a/testing/integration-tests/src/full_client/transactions.rs b/testing/integration-tests/src/full_client/transactions.rs index e32d7f344a..2345d88569 100644 --- a/testing/integration-tests/src/full_client/transactions.rs +++ b/testing/integration-tests/src/full_client/transactions.rs @@ -91,6 +91,7 @@ async fn v4_signed_encode_decode() -> Result<(), subxt::Error> { .await .unwrap() .sign(&dev::alice()) + .unwrap() .into_encoded(); let tx_bytes_cursor = &mut &*tx_bytes; @@ -126,6 +127,7 @@ async fn v5_general_encode_decode() -> Result<(), subxt::Error> { .await .unwrap() .sign(&dummy_signer) // No signature payload is added, but may be inserted into tx extensions. + .unwrap() .into_encoded(); let tx_bytes_cursor = &mut &*tx_bytes; diff --git a/testing/integration-tests/src/historic.rs b/testing/integration-tests/src/historic.rs index 3265db89b8..0a225f99ad 100644 --- a/testing/integration-tests/src/historic.rs +++ b/testing/integration-tests/src/historic.rs @@ -5,49 +5,51 @@ use crate::subxt_test; use subxt::{OnlineClient, config::PolkadotConfig}; -#[subxt_test] +#[subxt_test(timeout = 600)] async fn can_instantiate_client_across_historic_polkadot_runtimes() { - let api = connect_to_rpc_node(&[ - "wss://rpc.polkadot.io", - "wss://polkadot-public-rpc.blockops.network/ws", - "wss://1rpc.io/dot", - ]) - .await; + let futs = POLKADOT_SPEC_VERSION_BLOCKS + .into_iter() + .map(async move |block_num| { + tracing::info!("Connecting to RPC node at block {block_num}"); + let api = connect_to_rpc_node(&[ + "wss://rpc.polkadot.io", + "wss://polkadot-public-rpc.blockops.network/ws", + "wss://1rpc.io/dot", + ]) + .await; - let futs = POLKADOT_SPEC_VERSION_BLOCKS.into_iter().map(|block_num| { - let api = api.clone(); - async move { tracing::info!("Instantiating client at block {block_num}"); api.at_block(block_num) .await .unwrap_or_else(|e| panic!("Can't instantiate client at block {block_num}: {e}")); + tracing::info!(" -> Success Instantiating client at block {block_num}"); - } - }); + }); - run_with_concurrency(5, futs).await; + run_with_concurrency(10, futs).await; } -#[subxt_test] +#[subxt_test(timeout = 600)] async fn can_instantiate_client_across_historic_kusama_runtimes() { - let api = connect_to_rpc_node(&[ - "wss://kusama-public-rpc.blockops.network/ws", - "wss://rpc.ibp.network/kusama", - ]) - .await; + let futs = KUSAMA_SPEC_VERSION_BLOCKS + .into_iter() + .map(async move |block_num| { + tracing::info!("Connecting to RPC node at block {block_num}"); + let api = connect_to_rpc_node(&[ + "wss://rpc.ibp.network/kusama", + "wss://kusama-public-rpc.blockops.network/ws", + ]) + .await; - let futs = KUSAMA_SPEC_VERSION_BLOCKS.into_iter().map(|block_num| { - let api = api.clone(); - async move { tracing::info!("Instantiating client at block {block_num}"); api.at_block(block_num) .await .unwrap_or_else(|e| panic!("Can't instantiate client at block {block_num}: {e}")); + tracing::info!(" -> Success Instantiating client at block {block_num}"); - } - }); + }); - run_with_concurrency(5, futs).await; + run_with_concurrency(10, futs).await; } /// Runs at most `num_tasks` at once, running the next tasks only when currently running ones finish and free up space. @@ -70,11 +72,13 @@ async fn run_with_concurrency>( } async fn connect_to_rpc_node(urls: &[&'static str]) -> OnlineClient { - for url in urls { - let api = OnlineClient::::from_url(url).await; - match api { - Ok(api) => return api, - Err(e) => tracing::warn!("Error connecting to RPC node {url}: {e}"), + for _ in 0..3 { + for url in urls { + let api = OnlineClient::::from_url(url).await; + match api { + Ok(api) => return api, + Err(e) => tracing::warn!("Error connecting to RPC node {url}: {e}"), + } } } panic!("Could not connect to any RPC node") @@ -94,13 +98,11 @@ const POLKADOT_SPEC_VERSION_BLOCKS: [u64; 70] = [ ]; /// A list of entries denoting the historic Kusama RC blocks with spec and transaction versions changes. -/// This will change over time, but we only really care about historic blocks. -const KUSAMA_SPEC_VERSION_BLOCKS: [u64; 68] = [ +/// Our Kusama RPC nodes are _slow_ so we don't test _every_ version, only all of the oldest ones. +const KUSAMA_SPEC_VERSION_BLOCKS: [u64; 50] = [ 26668, 38244, 54248, 59658, 67650, 82191, 83237, 101503, 203466, 295787, 461692, 504329, 569326, 587686, 653183, 693487, 901442, 1375086, 1445458, 1472960, 1475648, 1491596, 1574408, 2064961, 2201991, 2671528, 2704202, 2728002, 2832534, 2962294, 3240000, 3274408, 3323565, 3534175, 3860281, 4143129, 4401242, 4841367, 5961600, 6137912, 6561855, 7100891, 7468792, - 7668600, 7812476, 8010981, 8073833, 8555825, 8945245, 9611377, 9625129, 9866422, 10403784, - 10960765, 11006614, 11404482, 11601803, 12008022, 12405451, 12665416, 12909508, 13109752, - 13555777, 13727747, 14248044, 14433840, 14645900, 15048375, + 7668600, 7812476, 8010981, 8073833, 8555825, 8945245, 9611377, ];