WIP: Add support in config for transaction creation and begin porting/implementing tx flows

This commit is contained in:
James Wilson
2025-12-02 17:34:24 +00:00
parent e4028e5a77
commit 03fd050b3c
12 changed files with 726 additions and 66 deletions
+14 -1
View File
@@ -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<Client, T> ClientAtBlock<Client, T> {
impl<Client, T> ClientAtBlock<Client, T>
where
T: Config,
Client: OfflineClientAtBlockT,
Client: OfflineClientAtBlockT<T>,
{
/// 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<Client, T> ClientAtBlock<Client, T>
@@ -45,6 +57,7 @@ where
T: Config,
Client: OnlineClientAtBlockT<T>,
{
/// The current block hash.
pub fn block_hash(&self) -> HashFor<T> {
self.client.block_hash()
}
+45 -8
View File
@@ -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<T: Config> OfflineClient<T> {
pub fn at_block(
&self,
block_number: impl Into<u64>,
) -> Result<ClientAtBlock<OfflineClientAtBlock, T>, OfflineClientAtBlockError> {
) -> Result<ClientAtBlock<OfflineClientAtBlock<T>, 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<T: Config> OfflineClient<T> {
.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<T: Config> {
metadata: Arc<Metadata>,
block_number: u64,
genesis_hash: Option<HashFor<T>>,
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<T: Config>: 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<Metadata>;
/// 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<HashFor<T>>;
/// 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<T: Config> OfflineClientAtBlockT<T> for OfflineClientAtBlock<T> {
fn metadata_ref(&self) -> &Metadata {
&self.metadata
}
fn metadata(&self) -> Arc<Metadata> {
self.metadata.clone()
}
fn block_number(&self) -> u64 {
self.block_number
}
fn genesis_hash(&self) -> Option<HashFor<T>> {
self.genesis_hash
}
fn spec_version(&self) -> u32 {
self.spec_version
}
fn transaction_version(&self) -> u32 {
self.transaction_version
}
}
+65 -21
View File
@@ -30,6 +30,9 @@ pub struct OnlineClient<T: Config> {
struct OnlineClientInner<T: Config> {
/// 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<T>,
/// The RPC methods used to communicate with the node.
backend: Arc<dyn Backend<T>>,
}
@@ -107,18 +110,31 @@ impl<T: Config> OnlineClient<T> {
.await
.map_err(OnlineClientError::CannotBuildCombinedBackend)?;
let backend: Arc<dyn Backend<T>> = 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<Arc<dyn Backend<T>>>) -> OnlineClient<T> {
OnlineClient {
pub async fn from_backend(
config: T,
backend: impl Into<Arc<dyn Backend<T>>>,
) -> Result<OnlineClient<T>, 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<T: Config> OnlineClient<T> {
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<T: Config> OnlineClient<T> {
})?;
(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<T: Config> OnlineClient<T> {
_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<T: Config> OnlineClient<T> {
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<T: Config> OnlineClient<T> {
/// This represents an online client at a specific block.
#[doc(hidden)]
pub trait OnlineClientAtBlockT<T: Config>: OfflineClientAtBlockT {
pub trait OnlineClientAtBlockT<T: Config>: OfflineClientAtBlockT<T> {
/// Return the RPC methods we'll use to interact with the node.
fn backend(&self) -> &dyn Backend<T>;
/// Return the block hash for the current block.
@@ -418,6 +446,10 @@ pub struct OnlineClientAtBlock<T: Config> {
backend: Arc<dyn Backend<T>>,
hasher: T::Hasher,
block_ref: BlockRef<HashFor<T>>,
block_number: u64,
spec_version: u32,
genesis_hash: HashFor<T>,
transaction_version: u32,
}
impl<T: Config> OnlineClientAtBlockT<T> for OnlineClientAtBlock<T> {
@@ -432,13 +464,25 @@ impl<T: Config> OnlineClientAtBlockT<T> for OnlineClientAtBlock<T> {
}
}
impl<T: Config> OfflineClientAtBlockT for OnlineClientAtBlock<T> {
impl<T: Config> OfflineClientAtBlockT<T> for OnlineClientAtBlock<T> {
fn metadata_ref(&self) -> &Metadata {
&self.metadata
}
fn metadata(&self) -> Arc<Metadata> {
self.metadata.clone()
}
fn block_number(&self) -> u64 {
self.block_number
}
fn genesis_hash(&self) -> Option<HashFor<T>> {
Some(self.genesis_hash)
}
fn spec_version(&self) -> u32 {
self.spec_version
}
fn transaction_version(&self) -> u32 {
self.transaction_version
}
}
fn get_legacy_types<T: Config>(
+15 -3
View File
@@ -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<HashFor<Self>> {
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<u32> {
fn spec_and_transaction_version_for_block_number(
&self,
_block_number: u64,
) -> Option<(u32, u32)> {
None
}
+4 -21
View File
@@ -21,31 +21,14 @@ use subxt_metadata::Metadata;
pub struct ClientState<T: Config> {
/// Genesis hash.
pub genesis_hash: HashFor<T>,
/// Runtime version.
pub runtime_version: RuntimeVersion,
/// Spec version.
pub spec_version: u32,
/// Transaction version.
pub transaction_version: u32,
/// Metadata.
pub metadata: Arc<Metadata>,
}
/// 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.
+10 -2
View File
@@ -71,12 +71,20 @@ impl Config for PolkadotConfig {
// because we need to pass the PolkadotConfig trait as a param.
type ExtrinsicParams = PolkadotExtrinsicParams<Self>;
fn genesis_hash(&self) -> Option<super::HashFor<Self>> {
self.0.genesis_hash()
}
fn legacy_types_for_spec_version(&'_ self, spec_version: u32) -> Option<TypeRegistrySet<'_>> {
self.0.legacy_types_for_spec_version(spec_version)
}
fn spec_version_for_block_number(&self, block_number: u64) -> Option<u32> {
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<Arc<Metadata>> {
+23 -8
View File
@@ -20,7 +20,8 @@ use subxt_metadata::Metadata;
/// Construct a [`SubstrateConfig`] using this.
pub struct SubstrateConfigBuilder {
legacy_types: Option<ChainTypeRegistry>,
spec_version_for_block_number: RangeMap<u64, u32>,
spec_and_transaction_version_for_block_number: RangeMap<u64, (u32, u32)>,
genesis_hash: Option<H256>,
metadata_for_spec_version: Mutex<HashMap<u32, Arc<Metadata>>>,
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<u64>,
/// 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<ChainTypeRegistry>,
spec_version_for_block_number: RangeMap<u64, u32>,
spec_and_transaction_version_for_block_number: RangeMap<u64, (u32, u32)>,
metadata_for_spec_version: Mutex<HashMap<u32, Arc<Metadata>>>,
}
@@ -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<u32> {
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()
}
+2 -2
View File
@@ -169,7 +169,7 @@ impl<T: Config> ExtrinsicParams<T> for CheckSpecVersion {
type Params = ();
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
Ok(CheckSpecVersion(client.runtime_version.spec_version))
Ok(CheckSpecVersion(client.spec_version))
}
}
@@ -240,7 +240,7 @@ impl<T: Config> ExtrinsicParams<T> for CheckTxVersion {
type Params = ();
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
Ok(CheckTxVersion(client.runtime_version.transaction_version))
Ok(CheckTxVersion(client.transaction_version))
}
}
+1
View File
@@ -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;
+260
View File
@@ -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<T>,
}
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<T>> 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<Call>(&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<Call>(&self, call: &Call) -> Result<Vec<u8>, 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<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, C>, 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<Call>(&self, call: &Call) -> Result<Transaction<T>, ExtrinsicError>
where
Call: Payload,
{
self.create_unsigned_at_version(call, 4)
}
/// Creates a V5 "bare" transaction without submitting it.
pub fn create_v5_bare<Call>(&self, call: &Call) -> Result<Transaction<T>, 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<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, 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<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, 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<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, 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<Call: Payload>(
&self,
call: &Call,
tx_version: u8,
) -> Result<Transaction<T>, 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<TransactionVersion, ExtrinsicError> {
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,
}
+272
View File
@@ -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<u8>,
) -> 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<Vec<u8>, 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<ValidationDetails<'_>> {
None
}
}
macro_rules! boxed_payload {
($ty:path) => {
impl<T: Payload + ?Sized> Payload for $ty {
fn encode_call_data_to(
&self,
metadata: &Metadata,
out: &mut Vec<u8>,
) -> Result<(), ExtrinsicError> {
self.as_ref().encode_call_data_to(metadata, out)
}
fn encode_call_data(&self, metadata: &Metadata) -> Result<Vec<u8>, ExtrinsicError> {
self.as_ref().encode_call_data(metadata)
}
fn validation_details(&self) -> Option<ValidationDetails<'_>> {
self.as_ref().validation_details()
}
}
};
}
boxed_payload!(Box<T>);
boxed_payload!(std::sync::Arc<T>);
boxed_payload!(std::rc::Rc<T>);
/// 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<CallData> {
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<Calldata> = DefaultPayload<Calldata>;
/// The type of a payload typically used for dynamic transaction payloads.
pub type DynamicPayload = DefaultPayload<Composite<()>>;
impl<CallData> DefaultPayload<CallData> {
/// Create a new [`DefaultPayload`].
pub fn new(
pallet_name: impl Into<String>,
call_name: impl Into<String>,
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<Composite<()>> {
/// 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<CallData: EncodeAsFields> Payload for DefaultPayload<CallData> {
fn encode_call_data_to(
&self,
metadata: &Metadata,
out: &mut Vec<u8>,
) -> 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<ValidationDetails<'_>> {
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<String>,
call_name: impl Into<String>,
call_data: impl Into<Composite<()>>,
) -> 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");
}
}
+15
View File
@@ -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<T: Config> {
/// 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;
}