mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 10:31:04 +00:00
Support constructing and submitting V5 transactions (#1931)
* TransactionExtensions basic support for V5 VerifySignature and renames * WIP: subxt-core v5 transaction support * Subxt to support V5 extrinsics * WIP tests failing with wsm trap error * Actually encode mortality to fix tx encode issue * fmt * rename to sign_with_account_and_signature * Add explicit methods for v4 and v5 ext construction * clippy * fix wasm example and no mut self where not needed * fix doc example * another doc fix * Add tests for tx encoding and fix v5 encode issue * add copyright and todo * refactor APIs to have clear v4/v5 split in core and slightly nicer split in subxt proper * rename Partial/SubmittableExtrinsic to *Transaction * Remove SignerT::address since it's not needed * doc fixes * fmt * doc fixes * Fix comment number * Clarify panic behaviour of inject_signature * fmt
This commit is contained in:
+204
-71
@@ -48,7 +48,9 @@
|
||||
//! tx::validate(&call, &state.metadata).unwrap();
|
||||
//!
|
||||
//! // We can build a signed transaction:
|
||||
//! let signed_call = tx::create_signed(&call, &state, &dev::alice(), params).unwrap();
|
||||
//! let signed_call = tx::create_v4_signed(&call, &state, params)
|
||||
//! .unwrap()
|
||||
//! .sign(&dev::alice());
|
||||
//!
|
||||
//! // And log it:
|
||||
//! println!("Tx: 0x{}", hex::encode(signed_call.encoded()));
|
||||
@@ -58,7 +60,7 @@ pub mod payload;
|
||||
pub mod signer;
|
||||
|
||||
use crate::config::{Config, ExtrinsicParams, ExtrinsicParamsEncoder, Hasher};
|
||||
use crate::error::{Error, MetadataError};
|
||||
use crate::error::{Error, ExtrinsicError, MetadataError};
|
||||
use crate::metadata::Metadata;
|
||||
use crate::utils::Encoded;
|
||||
use alloc::borrow::{Cow, ToOwned};
|
||||
@@ -89,6 +91,32 @@ pub fn validate<Call: Payload>(call: &Call, metadata: &Metadata) -> Result<(), E
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 suggested_version(metadata: &Metadata) -> Result<TransactionVersion, Error> {
|
||||
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.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Return the SCALE encoded bytes representing the call data of the transaction.
|
||||
pub fn call_data<Call: Payload>(call: &Call, metadata: &Metadata) -> Result<Vec<u8>, Error> {
|
||||
let mut bytes = Vec::new();
|
||||
@@ -96,10 +124,27 @@ pub fn call_data<Call: Payload>(call: &Call, metadata: &Metadata) -> Result<Vec<
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Creates an unsigned extrinsic without submitting it.
|
||||
pub fn create_unsigned<T: Config, Call: Payload>(
|
||||
/// Creates a V4 "unsigned" transaction without submitting it.
|
||||
pub fn create_v4_unsigned<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Transaction<T>, Error> {
|
||||
create_unsigned_at_version(call, 4, metadata)
|
||||
}
|
||||
|
||||
/// Creates a V5 "bare" transaction without submitting it.
|
||||
pub fn create_v5_bare<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Transaction<T>, Error> {
|
||||
create_unsigned_at_version(call, 5, metadata)
|
||||
}
|
||||
|
||||
// Create a V4 "unsigned" transaction or V5 "bare" transaction.
|
||||
fn create_unsigned_at_version<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
tx_version: u8,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Transaction<T>, Error> {
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
@@ -108,8 +153,8 @@ pub fn create_unsigned<T: Config, Call: Payload>(
|
||||
// 2. Encode extrinsic
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// transaction protocol version (4) (is not signed, so no 1 bit at the front).
|
||||
4u8.encode_to(&mut encoded_inner);
|
||||
// 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:
|
||||
@@ -126,15 +171,12 @@ pub fn create_unsigned<T: Config, Call: Payload>(
|
||||
Ok(Transaction::from_bytes(extrinsic))
|
||||
}
|
||||
|
||||
/// Create a partial extrinsic.
|
||||
///
|
||||
/// 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_signed<T: Config, Call: Payload>(
|
||||
/// Construct a v4 extrinsic, ready to be signed.
|
||||
pub fn create_v4_signed<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
client_state: &ClientState<T>,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransaction<T>, Error> {
|
||||
) -> Result<PartialTransactionV4<T>, Error> {
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
validate(call, &client_state.metadata)?;
|
||||
@@ -147,81 +189,83 @@ pub fn create_partial_signed<T: Config, Call: Payload>(
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T>>::new(client_state, params)?;
|
||||
|
||||
// Return these details, ready to construct a signed extrinsic from.
|
||||
Ok(PartialTransaction {
|
||||
Ok(PartialTransactionV4 {
|
||||
call_data,
|
||||
additional_and_extra_params,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a signed extrinsic without submitting it.
|
||||
///
|
||||
/// 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_signed<T, Call, Signer>(
|
||||
/// Construct a v5 "general" extrinsic, ready to be signed or emitted as is.
|
||||
pub fn create_v5_general<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
client_state: &ClientState<T>,
|
||||
signer: &Signer,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<Transaction<T>, Error>
|
||||
where
|
||||
T: Config,
|
||||
Call: Payload,
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
) -> Result<PartialTransactionV5<T>, Error> {
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
validate(call, &client_state.metadata)?;
|
||||
|
||||
// 2. Gather the "additional" and "extra" params along with the encoded call data,
|
||||
// ready to be signed.
|
||||
let partial_signed = create_partial_signed(call, client_state, params)?;
|
||||
// 2. Work out which TX extension version to target based on metadata (unless we
|
||||
// explicitly ask for a specific transaction version at a later step).
|
||||
let tx_extensions_version = client_state
|
||||
.metadata
|
||||
.extrinsic()
|
||||
.transaction_extensions_version();
|
||||
|
||||
// 3. Sign and construct an extrinsic from these details.
|
||||
Ok(partial_signed.sign(signer))
|
||||
// 3. SCALE encode call data to bytes (pallet u8, call u8, call params).
|
||||
let call_data = call_data(call, &client_state.metadata)?;
|
||||
|
||||
// 4. Construct our custom additional/extra params.
|
||||
let additional_and_extra_params =
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T>>::new(client_state, params)?;
|
||||
|
||||
// Return these details, ready to construct a signed extrinsic from.
|
||||
Ok(PartialTransactionV5 {
|
||||
call_data,
|
||||
additional_and_extra_params,
|
||||
tx_extensions_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// This represents a partially constructed transaction that needs signing before it is ready
|
||||
/// to submit. Use [`PartialTransaction::signer_payload()`] to return the payload that needs signing,
|
||||
/// [`PartialTransaction::sign()`] to sign the transaction using a [`SignerT`] impl, or
|
||||
/// [`PartialTransaction::sign_with_address_and_signature()`] to apply an existing signature and address
|
||||
/// to the transaction.
|
||||
pub struct PartialTransaction<T: Config> {
|
||||
/// A partially constructed V4 extrinsic, ready to be signed.
|
||||
pub struct PartialTransactionV4<T: Config> {
|
||||
call_data: Vec<u8>,
|
||||
additional_and_extra_params: T::ExtrinsicParams,
|
||||
}
|
||||
|
||||
impl<T: Config> PartialTransaction<T> {
|
||||
// Obtain bytes representing the signer payload and run call some function
|
||||
// with them. This can avoid an allocation in some cases when compared to
|
||||
// [`PartialExtrinsic::signer_payload()`].
|
||||
fn with_signer_payload<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: for<'a> FnOnce(Cow<'a, [u8]>) -> R,
|
||||
{
|
||||
let mut bytes = self.call_data.clone();
|
||||
self.additional_and_extra_params.encode_extra_to(&mut bytes);
|
||||
self.additional_and_extra_params
|
||||
.encode_additional_to(&mut bytes);
|
||||
if bytes.len() > 256 {
|
||||
f(Cow::Borrowed(blake2_256(&bytes).as_ref()))
|
||||
} else {
|
||||
f(Cow::Owned(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the signer payload for this extrinsic. These are the bytes that must
|
||||
/// be signed in order to produce a valid signature for the extrinsic.
|
||||
pub fn signer_payload(&self) -> Vec<u8> {
|
||||
self.with_signer_payload(|bytes| bytes.to_vec())
|
||||
}
|
||||
|
||||
impl<T: Config> PartialTransactionV4<T> {
|
||||
/// Return the bytes representing the call data for this partially constructed
|
||||
/// extrinsic.
|
||||
pub fn call_data(&self) -> &[u8] {
|
||||
&self.call_data
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransaction`] into a [`Transaction`], ready to submit.
|
||||
// Obtain bytes representing the signer payload and run call some function
|
||||
// with them. This can avoid an allocation in some cases.
|
||||
fn with_signer_payload<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: for<'a> FnOnce(Cow<'a, [u8]>) -> R,
|
||||
{
|
||||
let mut bytes = self.call_data.clone();
|
||||
self.additional_and_extra_params
|
||||
.encode_signer_payload_value_to(&mut bytes);
|
||||
self.additional_and_extra_params
|
||||
.encode_implicit_to(&mut bytes);
|
||||
|
||||
if bytes.len() > 256 {
|
||||
f(Cow::Borrowed(&blake2_256(&bytes)))
|
||||
} else {
|
||||
f(Cow::Owned(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the V4 signer payload for this extrinsic. These are the bytes that must
|
||||
/// be signed in order to produce a valid signature for the extrinsic.
|
||||
pub fn signer_payload(&self) -> Vec<u8> {
|
||||
self.with_signer_payload(|bytes| bytes.to_vec())
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV4`] into a V4 signed [`Transaction`], ready to submit.
|
||||
/// The provided `signer` is responsible for providing the "from" address for the transaction,
|
||||
/// as well as providing a signature to attach to it.
|
||||
pub fn sign<Signer>(&self, signer: &Signer) -> Transaction<T>
|
||||
@@ -231,30 +275,28 @@ impl<T: Config> PartialTransaction<T> {
|
||||
// Given our signer, we can sign the payload representing this extrinsic.
|
||||
let signature = self.with_signer_payload(|bytes| signer.sign(&bytes));
|
||||
// Now, use the signature and "from" address to build the extrinsic.
|
||||
self.sign_with_address_and_signature(&signer.address(), &signature)
|
||||
self.sign_with_account_and_signature(signer.account_id(), &signature)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransaction`] into a [`Transaction`], ready to submit.
|
||||
/// An address, and something representing a signature that can be SCALE encoded, are both
|
||||
/// needed in order to construct it. If you have a `Signer` to hand, you can use
|
||||
/// [`PartialTransaction::sign()`] instead.
|
||||
pub fn sign_with_address_and_signature(
|
||||
/// Convert this [`PartialTransactionV4`] into a V4 signed [`Transaction`], ready to submit.
|
||||
/// The provided `address` and `signature` will be used.
|
||||
pub fn sign_with_account_and_signature(
|
||||
&self,
|
||||
address: &T::Address,
|
||||
account_id: T::AccountId,
|
||||
signature: &T::Signature,
|
||||
) -> Transaction<T> {
|
||||
// Encode the extrinsic (into the format expected by protocol version 4)
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// "is signed" + transaction protocol version (4)
|
||||
(0b10000000 + 4u8).encode_to(&mut encoded_inner);
|
||||
// from address for signature
|
||||
let address: T::Address = account_id.into();
|
||||
address.encode_to(&mut encoded_inner);
|
||||
// the signature
|
||||
signature.encode_to(&mut encoded_inner);
|
||||
// attach custom extra params
|
||||
self.additional_and_extra_params
|
||||
.encode_extra_to(&mut encoded_inner);
|
||||
.encode_value_to(&mut encoded_inner);
|
||||
// and now, call data (remembering that it's been encoded already and just needs appending)
|
||||
encoded_inner.extend(&self.call_data);
|
||||
// now, prefix byte length:
|
||||
@@ -272,6 +314,97 @@ impl<T: Config> PartialTransaction<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A partially constructed V5 general extrinsic, ready to be signed or emitted as-is.
|
||||
pub struct PartialTransactionV5<T: Config> {
|
||||
call_data: Vec<u8>,
|
||||
additional_and_extra_params: T::ExtrinsicParams,
|
||||
tx_extensions_version: u8,
|
||||
}
|
||||
|
||||
impl<T: Config> PartialTransactionV5<T> {
|
||||
/// Return the bytes representing the call data for this partially constructed
|
||||
/// extrinsic.
|
||||
pub fn call_data(&self) -> &[u8] {
|
||||
&self.call_data
|
||||
}
|
||||
|
||||
/// Return the V5 signer payload for this extrinsic. These are the bytes that must
|
||||
/// be signed in order to produce a valid signature for the extrinsic.
|
||||
pub fn signer_payload(&self) -> [u8; 32] {
|
||||
let mut bytes = self.call_data.clone();
|
||||
|
||||
self.additional_and_extra_params
|
||||
.encode_signer_payload_value_to(&mut bytes);
|
||||
self.additional_and_extra_params
|
||||
.encode_implicit_to(&mut bytes);
|
||||
|
||||
blake2_256(&bytes)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`].
|
||||
///
|
||||
/// This transaction has not been explicitly signed. Use [`Self::sign`]
|
||||
/// or [`Self::sign_with_account_and_signature`] if you wish to provide a
|
||||
/// signature (this is usually a necessary step).
|
||||
pub fn to_transaction(&self) -> Transaction<T> {
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// "is general" + transaction protocol version (5)
|
||||
(0b01000000 + 5u8).encode_to(&mut encoded_inner);
|
||||
// Encode versions for the transaction extensions
|
||||
self.tx_extensions_version.encode_to(&mut encoded_inner);
|
||||
// Encode the actual transaction extensions values
|
||||
self.additional_and_extra_params
|
||||
.encode_value_to(&mut encoded_inner);
|
||||
// and now, call data (remembering that it's been encoded already and just needs appending)
|
||||
encoded_inner.extend(&self.call_data);
|
||||
// now, prefix byte length:
|
||||
let len = Compact(
|
||||
u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"),
|
||||
);
|
||||
let mut encoded = Vec::new();
|
||||
len.encode_to(&mut encoded);
|
||||
encoded.extend(encoded_inner);
|
||||
encoded
|
||||
};
|
||||
|
||||
// Return an extrinsic ready to be submitted.
|
||||
Transaction::from_bytes(extrinsic)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`] with a signature.
|
||||
///
|
||||
/// Signing the transaction injects the signature into the transaction extension data, which is why
|
||||
/// this method borrows self mutably. Signing repeatedly will override the previous signature.
|
||||
pub fn sign<Signer>(&mut self, signer: &Signer) -> Transaction<T>
|
||||
where
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
// Given our signer, we can sign the payload representing this extrinsic.
|
||||
let signature = signer.sign(&self.signer_payload());
|
||||
// Now, use the signature and "from" account to build the extrinsic.
|
||||
self.sign_with_account_and_signature(&signer.account_id(), &signature)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`] with a signature.
|
||||
/// Prefer [`Self::sign`] if you have a [`SignerT`] instance to use.
|
||||
///
|
||||
/// Signing the transaction injects the signature into the transaction extension data, which is why
|
||||
/// this method borrows self mutably. Signing repeatedly will override the previous signature.
|
||||
pub fn sign_with_account_and_signature(
|
||||
&mut self,
|
||||
account_id: &T::AccountId,
|
||||
signature: &T::Signature,
|
||||
) -> Transaction<T> {
|
||||
// Inject the signature into the transaction extensions
|
||||
// before constructing it.
|
||||
self.additional_and_extra_params
|
||||
.inject_signature(account_id, signature);
|
||||
|
||||
self.to_transaction()
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a signed transaction that's ready to be submitted.
|
||||
/// Use [`Transaction::encoded()`] or [`Transaction::into_encoded()`] to
|
||||
/// get the bytes for it, or [`Transaction::hash()`] to get the hash.
|
||||
|
||||
Reference in New Issue
Block a user