mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-07 13:08:03 +00:00
WIP: Add support in config for transaction creation and begin porting/implementing tx flows
This commit is contained in:
+14
-1
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user