From 03fd050b3cbf799ae7ec4dd9df24fa6879324e1d Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 2 Dec 2025 17:34:24 +0000 Subject: [PATCH] WIP: Add support in config for transaction creation and begin porting/implementing tx flows --- new/src/client.rs | 15 +- new/src/client/offline_client.rs | 53 ++++- new/src/client/online_client.rs | 86 +++++-- new/src/config.rs | 18 +- new/src/config/extrinsic_params.rs | 25 +-- new/src/config/polkadot.rs | 12 +- new/src/config/substrate.rs | 31 ++- new/src/config/transaction_extensions.rs | 4 +- new/src/lib.rs | 1 + new/src/transactions.rs | 260 ++++++++++++++++++++++ new/src/transactions/payload.rs | 272 +++++++++++++++++++++++ new/src/transactions/signer.rs | 15 ++ 12 files changed, 726 insertions(+), 66 deletions(-) create mode 100644 new/src/transactions.rs create mode 100644 new/src/transactions/payload.rs create mode 100644 new/src/transactions/signer.rs diff --git a/new/src/client.rs b/new/src/client.rs index fd5dfe9d97..11e01e3897 100644 --- a/new/src/client.rs +++ b/new/src/client.rs @@ -2,6 +2,7 @@ mod offline_client; mod online_client; use crate::config::{Config, HashFor}; +use crate::transactions::Transactions; use core::marker::PhantomData; use subxt_metadata::Metadata; @@ -33,11 +34,22 @@ impl ClientAtBlock { impl ClientAtBlock where T: Config, - Client: OfflineClientAtBlockT, + Client: OfflineClientAtBlockT, { + /// Construct transactions. + pub fn tx(&self) -> Transactions<'_, T, Client> { + Transactions::new(&self.client) + } + + /// Obtain a reference to the metadata. pub fn metadata_ref(&self) -> &Metadata { self.client.metadata_ref() } + + /// The current block number. + pub fn block_number(&self) -> u64 { + self.client.block_number() + } } impl ClientAtBlock @@ -45,6 +57,7 @@ where T: Config, Client: OnlineClientAtBlockT, { + /// The current block hash. pub fn block_hash(&self) -> HashFor { self.client.block_hash() } diff --git a/new/src/client/offline_client.rs b/new/src/client/offline_client.rs index 9698640185..62cf92bf06 100644 --- a/new/src/client/offline_client.rs +++ b/new/src/client/offline_client.rs @@ -1,5 +1,5 @@ use crate::client::ClientAtBlock; -use crate::config::Config; +use crate::config::{Config, HashFor}; use crate::error::OfflineClientAtBlockError; use std::sync::Arc; use subxt_metadata::Metadata; @@ -21,11 +21,11 @@ impl OfflineClient { pub fn at_block( &self, block_number: impl Into, - ) -> Result, OfflineClientAtBlockError> { + ) -> Result, T>, OfflineClientAtBlockError> { let block_number = block_number.into(); - let spec_version = self + let (spec_version, transaction_version) = self .config - .spec_version_for_block_number(block_number) + .spec_and_transaction_version_for_block_number(block_number) .ok_or(OfflineClientAtBlockError::SpecVersionNotFound { block_number })?; let metadata = self @@ -33,29 +33,66 @@ impl OfflineClient { .metadata_for_spec_version(spec_version) .ok_or(OfflineClientAtBlockError::MetadataNotFound { spec_version })?; - Ok(ClientAtBlock::new(OfflineClientAtBlock { metadata })) + let genesis_hash = self.config.genesis_hash(); + + let offline_client_at_block = OfflineClientAtBlock { + metadata, + block_number, + genesis_hash, + spec_version, + transaction_version, + }; + + Ok(ClientAtBlock::new(offline_client_at_block)) } } #[derive(Clone)] -pub struct OfflineClientAtBlock { +pub struct OfflineClientAtBlock { metadata: Arc, + block_number: u64, + genesis_hash: Option>, + spec_version: u32, + transaction_version: u32, } /// This represents an offline-only client at a specific block. #[doc(hidden)] -pub trait OfflineClientAtBlockT: Clone { +pub trait OfflineClientAtBlockT: Clone { /// Get a reference to the metadata appropriate for this block. fn metadata_ref(&self) -> &Metadata; /// Get a clone of the metadata appropriate for this block. fn metadata(&self) -> Arc; + /// The block number we're operating at. + fn block_number(&self) -> u64; + /// Return the genesis hash for the chain if it is known. + fn genesis_hash(&self) -> Option>; + /// The spec version at the current block. + fn spec_version(&self) -> u32; + /// The transaction version at the current block. + /// + /// Note: This is _not_ the same as the transaction version that + /// is encoded at the beginning of transactions (ie 4 or 5). + fn transaction_version(&self) -> u32; } -impl OfflineClientAtBlockT for OfflineClientAtBlock { +impl OfflineClientAtBlockT for OfflineClientAtBlock { fn metadata_ref(&self) -> &Metadata { &self.metadata } fn metadata(&self) -> Arc { self.metadata.clone() } + fn block_number(&self) -> u64 { + self.block_number + } + fn genesis_hash(&self) -> Option> { + self.genesis_hash + } + fn spec_version(&self) -> u32 { + self.spec_version + } + fn transaction_version(&self) -> u32 { + self.transaction_version + } } diff --git a/new/src/client/online_client.rs b/new/src/client/online_client.rs index 0f822b1361..61e9c594af 100644 --- a/new/src/client/online_client.rs +++ b/new/src/client/online_client.rs @@ -30,6 +30,9 @@ pub struct OnlineClient { struct OnlineClientInner { /// The configuration for this client. config: T, + /// Chain genesis hash. Needed to construct transactions, + /// so we obtain it up front on constructing this. + genesis_hash: HashFor, /// The RPC methods used to communicate with the node. backend: Arc>, } @@ -107,18 +110,31 @@ impl OnlineClient { .await .map_err(OnlineClientError::CannotBuildCombinedBackend)?; let backend: Arc> = Arc::new(backend); - Ok(OnlineClient::from_backend(config, backend)) + OnlineClient::from_backend(config, backend).await } /// Construct a new [`OnlineClient`] by providing an underlying [`Backend`] /// implementation to power it. - pub fn from_backend(config: T, backend: impl Into>>) -> OnlineClient { - OnlineClient { + pub async fn from_backend( + config: T, + backend: impl Into>>, + ) -> Result, OnlineClientError> { + let backend = backend.into(); + let genesis_hash = match config.genesis_hash() { + Some(hash) => hash, + None => backend + .genesis_hash() + .await + .map_err(OnlineClientError::CannotGetGenesisHash)?, + }; + + Ok(OnlineClient { inner: Arc::new(OnlineClientInner { config, + genesis_hash, backend: backend.into(), }), - } + }) } /// Obtain a stream of all blocks imported by the node. @@ -212,7 +228,7 @@ impl OnlineClient { let number_or_hash = number_or_hash.into(); // We are given either a block hash or number. We need both. - let (block_ref, block_num) = match number_or_hash { + let (block_ref, block_number) = match number_or_hash { BlockNumberOrRef::BlockRef(block_ref) => { let block_hash = block_ref.hash(); let block_header = self @@ -229,26 +245,29 @@ impl OnlineClient { })?; (block_ref, block_header.number()) } - BlockNumberOrRef::Number(block_num) => { + BlockNumberOrRef::Number(block_number) => { let block_ref = self .inner .backend - .block_number_to_hash(block_num) + .block_number_to_hash(block_number) .await .map_err(|e| OnlineClientAtBlockError::CannotGetBlockHash { - block_number: block_num, + block_number, reason: e, })? - .ok_or(OnlineClientAtBlockError::BlockNotFound { - block_number: block_num, - })?; - (block_ref, block_num) + .ok_or(OnlineClientAtBlockError::BlockNotFound { block_number })?; + (block_ref, block_number) } }; let block_hash = block_ref.hash(); // Obtain the spec version so that we know which metadata to use at this block. - let spec_version = match self.inner.config.spec_version_for_block_number(block_num) { + // Obtain the transaction version because it's required for constructing extrinsics. + let (spec_version, transaction_version) = match self + .inner + .config + .spec_and_transaction_version_for_block_number(block_number) + { Some(version) => version, None => { let spec_version_bytes = self @@ -267,13 +286,18 @@ impl OnlineClient { _impl_name: String, _authoring_version: u32, spec_version: u32, + _impl_version: u32, + _apis: Vec<([u8; 8], u32)>, + transaction_version: u32, } - SpecVersionHeader::decode(&mut &spec_version_bytes[..]) - .map_err(|e| OnlineClientAtBlockError::CannotDecodeSpecVersion { - block_hash: block_hash.into(), - reason: e, - })? - .spec_version + let version = + SpecVersionHeader::decode(&mut &spec_version_bytes[..]).map_err(|e| { + OnlineClientAtBlockError::CannotDecodeSpecVersion { + block_hash: block_hash.into(), + reason: e, + } + })?; + (version.spec_version, version.transaction_version) } }; @@ -391,6 +415,10 @@ impl OnlineClient { metadata, backend: self.inner.backend.clone(), block_ref, + block_number, + spec_version, + genesis_hash: self.inner.genesis_hash, + transaction_version, }; Ok(ClientAtBlock { @@ -402,7 +430,7 @@ impl OnlineClient { /// This represents an online client at a specific block. #[doc(hidden)] -pub trait OnlineClientAtBlockT: OfflineClientAtBlockT { +pub trait OnlineClientAtBlockT: OfflineClientAtBlockT { /// Return the RPC methods we'll use to interact with the node. fn backend(&self) -> &dyn Backend; /// Return the block hash for the current block. @@ -418,6 +446,10 @@ pub struct OnlineClientAtBlock { backend: Arc>, hasher: T::Hasher, block_ref: BlockRef>, + block_number: u64, + spec_version: u32, + genesis_hash: HashFor, + transaction_version: u32, } impl OnlineClientAtBlockT for OnlineClientAtBlock { @@ -432,13 +464,25 @@ impl OnlineClientAtBlockT for OnlineClientAtBlock { } } -impl OfflineClientAtBlockT for OnlineClientAtBlock { +impl OfflineClientAtBlockT for OnlineClientAtBlock { fn metadata_ref(&self) -> &Metadata { &self.metadata } fn metadata(&self) -> Arc { self.metadata.clone() } + fn block_number(&self) -> u64 { + self.block_number + } + fn genesis_hash(&self) -> Option> { + Some(self.genesis_hash) + } + fn spec_version(&self) -> u32 { + self.spec_version + } + fn transaction_version(&self) -> u32 { + self.transaction_version + } } fn get_legacy_types( diff --git a/new/src/config.rs b/new/src/config.rs index 20818bf150..06333700e9 100644 --- a/new/src/config.rs +++ b/new/src/config.rs @@ -26,7 +26,7 @@ use subxt_metadata::Metadata; use subxt_rpcs::RpcConfig; pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder}; -pub use extrinsic_params::{ExtrinsicParams, ExtrinsicParamsEncoder}; +pub use extrinsic_params::{ClientState, ExtrinsicParams, ExtrinsicParamsEncoder}; pub use polkadot::{PolkadotConfig, PolkadotExtrinsicParams, PolkadotExtrinsicParamsBuilder}; pub use substrate::{SubstrateConfig, SubstrateExtrinsicParams, SubstrateExtrinsicParamsBuilder}; pub use transaction_extensions::TransactionExtension; @@ -59,11 +59,23 @@ pub trait Config: Clone + Debug + Sized + Send + Sync + 'static { /// can then be used to hash things at that block. type Hasher: Hasher; - /// Return the spec version for a given block number, if available. + /// The starting hash for the chain we're connecting to. This is required for constructing transactions. + /// + /// If not provided by the config implementation, it will be obtained from the chain in the case of the + /// [`crate::client::OnlineClient`]. It must be provided to construct transactions via the + /// [`crate::client::OfflineClient`], else an error will be returned. + fn genesis_hash(&self) -> Option> { + None + } + + /// Return a tuple of the spec version and then transaction version for a given block number, if available. /// /// The [`crate::client::OnlineClient`] will look this up on chain if it's not available here, /// but the [`crate::client::OfflineClient`] will error if this is not available for the required block number. - fn spec_version_for_block_number(&self, _block_number: u64) -> Option { + fn spec_and_transaction_version_for_block_number( + &self, + _block_number: u64, + ) -> Option<(u32, u32)> { None } diff --git a/new/src/config/extrinsic_params.rs b/new/src/config/extrinsic_params.rs index 8261d015d4..3a7f5b5980 100644 --- a/new/src/config/extrinsic_params.rs +++ b/new/src/config/extrinsic_params.rs @@ -21,31 +21,14 @@ use subxt_metadata::Metadata; pub struct ClientState { /// Genesis hash. pub genesis_hash: HashFor, - /// Runtime version. - pub runtime_version: RuntimeVersion, + /// Spec version. + pub spec_version: u32, + /// Transaction version. + pub transaction_version: u32, /// Metadata. pub metadata: Arc, } -/// Runtime version information needed to submit transactions. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RuntimeVersion { - /// Version of the runtime specification. A full-node will not attempt to use its native - /// runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, - /// `spec_version` and `authoring_version` are the same between Wasm and native. - pub spec_version: u32, - /// All existing dispatches are fully compatible when this number doesn't change. If this - /// number changes, then `spec_version` must change, also. - /// - /// This number must change when an existing dispatchable (module ID, dispatch ID) is changed, - /// either through an alteration in its user-level semantics, a parameter - /// added/removed/changed, a dispatchable being removed, a module being removed, or a - /// dispatchable/module changing its index. - /// - /// It need *not* change when a new module is added or when a dispatchable is added. - pub transaction_version: u32, -} - /// 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. diff --git a/new/src/config/polkadot.rs b/new/src/config/polkadot.rs index b5f907fd6d..3c5e5435e7 100644 --- a/new/src/config/polkadot.rs +++ b/new/src/config/polkadot.rs @@ -71,12 +71,20 @@ impl Config for PolkadotConfig { // because we need to pass the PolkadotConfig trait as a param. type ExtrinsicParams = PolkadotExtrinsicParams; + fn genesis_hash(&self) -> Option> { + self.0.genesis_hash() + } + fn legacy_types_for_spec_version(&'_ self, spec_version: u32) -> Option> { self.0.legacy_types_for_spec_version(spec_version) } - fn spec_version_for_block_number(&self, block_number: u64) -> Option { - self.0.spec_version_for_block_number(block_number) + fn spec_and_transaction_version_for_block_number( + &self, + block_number: u64, + ) -> Option<(u32, u32)> { + self.0 + .spec_and_transaction_version_for_block_number(block_number) } fn metadata_for_spec_version(&self, spec_version: u32) -> Option> { diff --git a/new/src/config/substrate.rs b/new/src/config/substrate.rs index c116dab15f..360f4e47e7 100644 --- a/new/src/config/substrate.rs +++ b/new/src/config/substrate.rs @@ -20,7 +20,8 @@ use subxt_metadata::Metadata; /// Construct a [`SubstrateConfig`] using this. pub struct SubstrateConfigBuilder { legacy_types: Option, - spec_version_for_block_number: RangeMap, + spec_and_transaction_version_for_block_number: RangeMap, + genesis_hash: Option, metadata_for_spec_version: Mutex>>, use_old_v9_hashers_before_spec_version: u32, } @@ -36,12 +37,19 @@ impl SubstrateConfigBuilder { pub fn new() -> Self { SubstrateConfigBuilder { legacy_types: None, - spec_version_for_block_number: RangeMap::empty(), + genesis_hash: None, + spec_and_transaction_version_for_block_number: RangeMap::empty(), metadata_for_spec_version: Mutex::new(HashMap::new()), use_old_v9_hashers_before_spec_version: 0, } } + /// Set the genesis hash for this chain. + pub fn set_genesis_hash(mut self, genesis_hash: H256) -> Self { + self.genesis_hash = Some(genesis_hash); + self + } + /// Set the legacy types to use for this configuration. This enables support for /// blocks produced by Runtimes that emit metadata older than V14. pub fn set_legacy_types(mut self, legacy_types: ChainTypeRegistry) -> Self { @@ -73,9 +81,10 @@ impl SubstrateConfigBuilder { let start = version_for_range.block_range.start; let end = version_for_range.block_range.end; let spec_version = version_for_range.spec_version; - m = m.add_range(start, end, spec_version); + let transaction_version = version_for_range.transaction_version; + m = m.add_range(start, end, (spec_version, transaction_version)); } - self.spec_version_for_block_number = m.build(); + self.spec_and_transaction_version_for_block_number = m.build(); self } @@ -91,7 +100,8 @@ impl SubstrateConfigBuilder { SubstrateConfig { inner: Arc::new(SubstrateConfigInner { legacy_types: self.legacy_types, - spec_version_for_block_number: self.spec_version_for_block_number, + spec_and_transaction_version_for_block_number: self + .spec_and_transaction_version_for_block_number, metadata_for_spec_version: self.metadata_for_spec_version, }), } @@ -107,6 +117,8 @@ pub struct SpecVersionForRange { pub block_range: std::ops::Range, /// The spec version at this block range. pub spec_version: u32, + /// The transaction version at this block range. + pub transaction_version: u32, } /// Configuration that's suitable for standard Substrate chains (ie those @@ -119,7 +131,7 @@ pub struct SubstrateConfig { #[derive(Debug)] struct SubstrateConfigInner { legacy_types: Option, - spec_version_for_block_number: RangeMap, + spec_and_transaction_version_for_block_number: RangeMap, metadata_for_spec_version: Mutex>>, } @@ -146,9 +158,12 @@ impl Config for SubstrateConfig { .map(|types| types.for_spec_version(spec_version as u64)) } - fn spec_version_for_block_number(&self, block_number: u64) -> Option { + fn spec_and_transaction_version_for_block_number( + &self, + block_number: u64, + ) -> Option<(u32, u32)> { self.inner - .spec_version_for_block_number + .spec_and_transaction_version_for_block_number .get(block_number) .copied() } diff --git a/new/src/config/transaction_extensions.rs b/new/src/config/transaction_extensions.rs index 617ccbd3be..a97ca07142 100644 --- a/new/src/config/transaction_extensions.rs +++ b/new/src/config/transaction_extensions.rs @@ -169,7 +169,7 @@ impl ExtrinsicParams for CheckSpecVersion { type Params = (); fn new(client: &ClientState, _params: Self::Params) -> Result { - Ok(CheckSpecVersion(client.runtime_version.spec_version)) + Ok(CheckSpecVersion(client.spec_version)) } } @@ -240,7 +240,7 @@ impl ExtrinsicParams for CheckTxVersion { type Params = (); fn new(client: &ClientState, _params: Self::Params) -> Result { - Ok(CheckTxVersion(client.runtime_version.transaction_version)) + Ok(CheckTxVersion(client.transaction_version)) } } diff --git a/new/src/lib.rs b/new/src/lib.rs index 80ce58a3da..fcba35dc36 100644 --- a/new/src/lib.rs +++ b/new/src/lib.rs @@ -36,6 +36,7 @@ pub mod backend; pub mod client; pub mod config; pub mod error; +pub mod transactions; pub mod utils; // pub mod book; // pub mod blocks; diff --git a/new/src/transactions.rs b/new/src/transactions.rs new file mode 100644 index 0000000000..8c9fd53dae --- /dev/null +++ b/new/src/transactions.rs @@ -0,0 +1,260 @@ +mod payload; +mod signer; + +use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT}; +use crate::config::{ClientState, Config}; +use crate::error::ExtrinsicError; +use codec::Compact; +use core::marker::PhantomData; + +pub use payload::Payload; +pub use signer::Signer; + +/// A client for working with transactions. +#[derive(Clone)] +pub struct Transactions<'atblock, T, Client> { + client: &'atblock Client, + marker: PhantomData, +} + +impl<'atblock, T, Client> Transactions<'atblock, T, Client> { + pub(crate) fn new(client: &'atblock Client) -> Self { + Transactions { + client, + marker: PhantomData, + } + } +} + +impl<'atblock, T: Config, C: OfflineClientAtBlockT> Transactions<'atblock, T, C> { + /// Run the validation logic against some transaction you'd like to submit. Returns `Ok(())` + /// if the call is valid (or if it's not possible to check since the call has no validation hash). + /// Return an error if the call was not valid or something went wrong trying to validate it (ie + /// the pallet or call in question do not exist at all). + pub fn validate(&self, call: &Call) -> Result<(), ExtrinsicError> + where + Call: Payload, + { + let Some(details) = call.validation_details() else { + return Ok(()); + }; + + let pallet_name = details.pallet_name; + let call_name = details.call_name; + + let expected_hash = self + .client + .metadata_ref() + .pallet_by_name(pallet_name) + .ok_or_else(|| ExtrinsicError::PalletNameNotFound(pallet_name.to_string()))? + .call_hash(call_name) + .ok_or_else(|| ExtrinsicError::CallNameNotFound { + pallet_name: pallet_name.to_string(), + call_name: call_name.to_string(), + })?; + + if details.hash != expected_hash { + Err(ExtrinsicError::IncompatibleCodegen) + } else { + Ok(()) + } + } + + /// Return the SCALE encoded bytes representing the call data of the transaction. + pub fn call_data(&self, call: &Call) -> Result, ExtrinsicError> + where + Call: Payload, + { + let mut bytes = Vec::new(); + let metadata = self.client.metadata_ref(); + call.encode_call_data_to(metadata, &mut bytes)?; + Ok(bytes) + } + + /// Creates an unsigned transaction without submitting it. Depending on the metadata, we might end + /// up constructing either a v4 or v5 transaction. See [`Self::create_v4_unsigned`] or + /// [`Self::create_v5_bare`] if you'd like to explicitly create an unsigned transaction of a certain version. + pub fn create_unsigned( + &self, + call: &Call, + ) -> Result, ExtrinsicError> + where + Call: Payload, + { + let tx = match self.default_transaction_version()? { + TransactionVersion::V4 => self.create_v4_unsigned(call), + TransactionVersion::V5 => self.create_v5_bare(call), + }?; + + Ok(SubmittableTransaction { + client: self.client.clone(), + inner: tx, + }) + } + + /// Creates a V4 "unsigned" transaction without submitting it. + pub fn create_v4_unsigned(&self, call: &Call) -> Result, ExtrinsicError> + where + Call: Payload, + { + self.create_unsigned_at_version(call, 4) + } + + /// Creates a V5 "bare" transaction without submitting it. + pub fn create_v5_bare(&self, call: &Call) -> Result, ExtrinsicError> + where + Call: Payload, + { + self.create_unsigned_at_version(call, 5) + } + + /// Create a partial transaction. Depending on the metadata, we might end up constructing either a v4 or + /// v5 transaction. See [`subxt_core::tx`] if you'd like to manually pick the version to construct + /// + /// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_. + /// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values. + pub fn create_partial_offline( + &self, + call: &Call, + params: >::Params, + ) -> Result, ExtrinsicError> + where + Call: Payload, + { + let metadata = self.client.metadata(); + let client_state = ClientState { + genesis_hash: self.client.genesis_hash(), + spec_version: self.client.spec_version(), + transaction_version: self.client.transaction_version(), + metadata: self.client.metadata(), + }; + + let tx = match self.default_transaction_version(&metadata)? { + TransactionVersion::V4 => { + PartialTransactionInner::V4(self.create_v4_signed(call, &client_state, params)?) + } + TransactionVersion::V5 => { + PartialTransactionInner::V5(self.create_v5_general(call, &client_state, params)?) + } + }; + + Ok(PartialTransaction { + client: self.client.clone(), + inner: tx, + }) + } + + /// Create a v4 partial transaction, ready to sign. + /// + /// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_. + /// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values. + /// + /// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this will pick the + /// most suitable one for the given chain. + pub fn create_v4_partial_offline( + &self, + call: &Call, + params: >::Params, + ) -> Result, ExtrinsicError> + where + Call: Payload, + { + let tx = PartialTransactionInner::V4(subxt_core::tx::create_v4_signed( + call, + &self.client.client_state(), + params, + )?); + + Ok(PartialTransaction { + client: self.client.clone(), + inner: tx, + }) + } + + /// Create a v5 partial transaction, ready to sign. + /// + /// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_. + /// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values. + /// + /// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this will pick the + /// most suitable one for the given chain. + pub fn create_v5_partial_offline( + &self, + call: &Call, + params: >::Params, + ) -> Result, ExtrinsicError> + where + Call: Payload, + { + let tx = PartialTransactionInner::V5(subxt_core::tx::create_v5_general( + call, + &self.client.client_state(), + params, + )?); + + Ok(PartialTransaction { + client: self.client.clone(), + inner: tx, + }) + } + + // Create a V4 "unsigned" transaction or V5 "bare" transaction. + fn create_unsigned_at_version( + &self, + call: &Call, + tx_version: u8, + ) -> 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.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(Transaction::from_bytes(extrinsic)) + } + + /// Returns the suggested transaction versions to build for a given chain, or an error + /// if Subxt doesn't support any version expected by the chain. + /// + /// If the result is [`TransactionVersion::V4`], use the `v4` methods in this module. If it's + /// [`TransactionVersion::V5`], use the `v5` ones. + pub fn default_transaction_version(&self) -> Result { + let metadata = self.client.metadata_ref(); + let versions = metadata.extrinsic().supported_versions(); + + if versions.contains(&4) { + Ok(TransactionVersion::V4) + } else if versions.contains(&5) { + Ok(TransactionVersion::V5) + } else { + Err(ExtrinsicError::UnsupportedVersion) + } + } +} + +/// The transaction versions supported by Subxt. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum TransactionVersion { + /// v4 transactions (signed and unsigned transactions) + V4, + /// v5 transactions (bare and general transactions) + V5, +} diff --git a/new/src/transactions/payload.rs b/new/src/transactions/payload.rs new file mode 100644 index 0000000000..6dde8893a8 --- /dev/null +++ b/new/src/transactions/payload.rs @@ -0,0 +1,272 @@ +// Copyright 2019-2024 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! 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>; + + /// 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) + } + + /// Returns the details needed to validate the call, which + /// include a statically generated hash, the pallet name, + /// and the call name. + fn validation_details(&self) -> Option> { + None + } +} + +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) + } + fn encode_call_data(&self, metadata: &Metadata) -> Result, ExtrinsicError> { + self.as_ref().encode_call_data(metadata) + } + fn validation_details(&self) -> Option> { + self.as_ref().validation_details() + } + } + }; +} + +boxed_payload!(Box); +boxed_payload!(std::sync::Arc); +boxed_payload!(std::rc::Rc); + +/// Details required to validate the shape of a transaction payload against some metadata. +pub struct ValidationDetails<'a> { + /// The pallet name. + pub pallet_name: &'a str, + /// The call name. + pub call_name: &'a str, + /// A hash (this is generated at compile time in our codegen) + /// to compare against the runtime code. + pub hash: [u8; 32], +} + +/// A transaction payload containing some generic `CallData`. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct DefaultPayload { + pallet_name: Cow<'static, str>, + call_name: Cow<'static, str>, + call_data: CallData, + validation_hash: Option<[u8; 32]>, +} + +/// The payload type used by static codegen. +pub type StaticPayload = DefaultPayload; +/// The type of a payload typically used for dynamic transaction payloads. +pub type DynamicPayload = DefaultPayload>; + +impl DefaultPayload { + /// Create a new [`DefaultPayload`]. + pub fn new( + pallet_name: impl Into, + call_name: impl Into, + call_data: CallData, + ) -> Self { + DefaultPayload { + pallet_name: Cow::Owned(pallet_name.into()), + call_name: Cow::Owned(call_name.into()), + call_data, + validation_hash: None, + } + } + + /// Create a new [`DefaultPayload`] using static strings for the pallet and call name. + /// This is only expected to be used from codegen. + #[doc(hidden)] + pub fn new_static( + pallet_name: &'static str, + call_name: &'static str, + call_data: CallData, + validation_hash: [u8; 32], + ) -> Self { + DefaultPayload { + pallet_name: Cow::Borrowed(pallet_name), + call_name: Cow::Borrowed(call_name), + call_data, + validation_hash: Some(validation_hash), + } + } + + /// Do not validate this call prior to submitting it. + pub fn unvalidated(self) -> Self { + Self { + validation_hash: None, + ..self + } + } + + /// Returns the call data. + pub fn call_data(&self) -> &CallData { + &self.call_data + } + + /// Returns the pallet name. + pub fn pallet_name(&self) -> &str { + &self.pallet_name + } + + /// Returns the call name. + pub fn call_name(&self) -> &str { + &self.call_name + } +} + +impl DefaultPayload> { + /// Convert the dynamic `Composite` payload into a [`Value`]. + /// This is useful if you want to use this as an argument for a + /// larger dynamic call that wants to use this as a nested call. + pub fn into_value(self) -> Value<()> { + let call = Value { + context: (), + value: ValueDef::Variant(Variant { + name: self.call_name.into_owned(), + values: self.call_data, + }), + }; + + Value::unnamed_variant(self.pallet_name, [call]) + } +} + +impl Payload for DefaultPayload { + 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; + + pallet_index.encode_to(out); + call_index.encode_to(out); + + let mut fields = call + .fields + .iter() + .map(|f| scale_encode::Field::new(f.ty.id, f.name.as_deref())); + + self.call_data + .encode_as_fields_to(&mut fields, metadata.types(), out) + .map_err(ExtrinsicError::CannotEncodeCallData)?; + Ok(()) + } + + fn validation_details(&self) -> Option> { + self.validation_hash.map(|hash| ValidationDetails { + pallet_name: &self.pallet_name, + call_name: &self.call_name, + hash, + }) + } +} + +/// Construct a transaction at runtime; essentially an alias to [`DefaultPayload::new()`] +/// which provides a [`Composite`] value for the call data. +pub fn dynamic( + pallet_name: impl Into, + call_name: impl Into, + call_data: impl Into>, +) -> DynamicPayload { + DefaultPayload::new(pallet_name, call_name, call_data.into()) +} + +#[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 = DefaultPayload::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 = DefaultPayload::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/new/src/transactions/signer.rs b/new/src/transactions/signer.rs new file mode 100644 index 0000000000..0eddd8718c --- /dev/null +++ b/new/src/transactions/signer.rs @@ -0,0 +1,15 @@ +use crate::config::Config; + +/// Signing transactions requires a [`Signer`]. This is responsible for +/// providing the "from" account that the transaction is being signed by, +/// as well as actually signing a SCALE encoded payload. +pub trait Signer { + /// Return the "from" account ID. + fn account_id(&self) -> T::AccountId; + + /// Takes a signer payload for an extrinsic, and returns a signature based on it. + /// + /// Some signers may fail, for instance because the hardware on which the keys are located has + /// refused the operation. + fn sign(&self, signer_payload: &[u8]) -> T::Signature; +}