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:
James Wilson
2025-03-11 11:14:27 +00:00
committed by GitHub
parent dcb9c27fcc
commit b6b9ac65c7
50 changed files with 1368 additions and 781 deletions
+3 -3
View File
@@ -17,7 +17,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Decode each signed extrinsic in the block dynamically
let extrinsics = block.extrinsics().await?;
for ext in extrinsics.iter() {
let Some(signed_extensions) = ext.signed_extensions() else {
let Some(transaction_extensions) = ext.transaction_extensions() else {
continue; // we do not look at inherents in this example
};
@@ -25,8 +25,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let fields = ext.field_values()?;
println!(" {}/{}", meta.pallet.name(), meta.variant.name);
println!(" Signed Extensions:");
for signed_ext in signed_extensions.iter() {
println!(" Transaction Extensions:");
for signed_ext in transaction_extensions.iter() {
// We only want to take a look at these 3 signed extensions, because the others all just have unit fields.
if ["CheckMortality", "CheckNonce", "ChargeTransactionPayment"]
.contains(&signed_ext.name())
+1 -1
View File
@@ -30,7 +30,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
for transfer in extrinsics.find::<TransferKeepAlive>() {
let transfer = transfer?;
let Some(extensions) = transfer.details.signed_extensions() else {
let Some(extensions) = transfer.details.transaction_extensions() else {
panic!("TransferKeepAlive should be signed")
};
+5 -5
View File
@@ -48,11 +48,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!(" {}", event_values);
}
println!(" Signed Extensions:");
if let Some(signed_extensions) = ext.signed_extensions() {
for signed_extension in signed_extensions.iter() {
let name = signed_extension.name();
let value = signed_extension.value()?.to_string();
println!(" Transaction Extensions:");
if let Some(transaction_extensions) = ext.transaction_extensions() {
for transaction_extension in transaction_extensions.iter() {
let name = transaction_extension.name();
let value = transaction_extension.value()?.to_string();
println!(" {name}: {value}");
}
}
+3 -6
View File
@@ -38,12 +38,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let current_nonce = rpc
.system_account_next_index(&alice.public_key().into())
.await?;
let current_header = rpc.chain_get_header(None).await?.unwrap();
let ext_params = Params::new()
.mortal(&current_header, 8)
.nonce(current_nonce)
.build();
let ext_params = Params::new().mortal(8).nonce(current_nonce).build();
let balance_transfer = polkadot::tx()
.balances()
@@ -51,7 +47,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ext_hash = api
.tx()
.create_signed_offline(&balance_transfer, &alice, ext_params)?
.create_partial_offline(&balance_transfer, ext_params)?
.sign(&alice)
.submit()
.await?;
+5 -6
View File
@@ -2,7 +2,8 @@
use codec::Encode;
use subxt::client::ClientState;
use subxt::config::{
Config, ExtrinsicParams, ExtrinsicParamsEncoder, ExtrinsicParamsError, RefineParams,
transaction_extensions::Params, Config, ExtrinsicParams, ExtrinsicParamsEncoder,
ExtrinsicParamsError,
};
use subxt_signer::sr25519::dev;
@@ -53,7 +54,7 @@ impl CustomExtrinsicParamsBuilder {
}
}
impl<T: Config> RefineParams<T> for CustomExtrinsicParamsBuilder {}
impl<T: Config> Params<T> for CustomExtrinsicParamsBuilder {}
// Describe how to fetch and then encode the params:
impl<T: Config> ExtrinsicParams<T> for CustomExtrinsicParams<T> {
@@ -69,14 +70,12 @@ impl<T: Config> ExtrinsicParams<T> for CustomExtrinsicParams<T> {
}
}
impl<T: Config> RefineParams<T> for CustomExtrinsicParams<T> {}
// Encode the relevant params when asked:
impl<T: Config> ExtrinsicParamsEncoder for CustomExtrinsicParams<T> {
fn encode_extra_to(&self, v: &mut Vec<u8>) {
fn encode_value_to(&self, v: &mut Vec<u8>) {
(self.tip, self.foo).encode_to(v);
}
fn encode_additional_to(&self, v: &mut Vec<u8>) {
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
self.genesis_hash.encode_to(v)
}
}
@@ -3,7 +3,7 @@ use codec::Encode;
use scale_encode::EncodeAsType;
use scale_info::PortableRegistry;
use subxt::client::ClientState;
use subxt::config::signed_extensions;
use subxt::config::transaction_extensions;
use subxt::config::{
Config, DefaultExtrinsicParamsBuilder, ExtrinsicParams, ExtrinsicParamsEncoder,
ExtrinsicParamsError,
@@ -25,53 +25,54 @@ impl Config for CustomConfig {
type Signature = subxt::utils::MultiSignature;
type Hasher = subxt::config::substrate::BlakeTwo256;
type Header = subxt::config::substrate::SubstrateHeader<u32, Self::Hasher>;
type ExtrinsicParams = signed_extensions::AnyOf<
type ExtrinsicParams = transaction_extensions::AnyOf<
Self,
(
// Load in the existing signed extensions we're interested in
// (if the extension isn't actually needed it'll just be ignored):
signed_extensions::CheckSpecVersion,
signed_extensions::CheckTxVersion,
signed_extensions::CheckNonce,
signed_extensions::CheckGenesis<Self>,
signed_extensions::CheckMortality<Self>,
signed_extensions::ChargeAssetTxPayment<Self>,
signed_extensions::ChargeTransactionPayment,
signed_extensions::CheckMetadataHash,
transaction_extensions::VerifySignature<Self>,
transaction_extensions::CheckSpecVersion,
transaction_extensions::CheckTxVersion,
transaction_extensions::CheckNonce,
transaction_extensions::CheckGenesis<Self>,
transaction_extensions::CheckMortality<Self>,
transaction_extensions::ChargeAssetTxPayment<Self>,
transaction_extensions::ChargeTransactionPayment,
transaction_extensions::CheckMetadataHash,
// And add a new one of our own:
CustomSignedExtension,
CustomTransactionExtension,
),
>;
type AssetId = u32;
}
// Our custom signed extension doesn't do much:
pub struct CustomSignedExtension;
pub struct CustomTransactionExtension;
// Give the extension a name; this allows `AnyOf` to look it
// up in the chain metadata in order to know when and if to use it.
impl<T: Config> signed_extensions::SignedExtension<T> for CustomSignedExtension {
impl<T: Config> transaction_extensions::TransactionExtension<T> for CustomTransactionExtension {
type Decoded = ();
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
identifier == "CustomSignedExtension"
identifier == "CustomTransactionExtension"
}
}
// Gather together any params we need for our signed extension, here none.
impl<T: Config> ExtrinsicParams<T> for CustomSignedExtension {
impl<T: Config> ExtrinsicParams<T> for CustomTransactionExtension {
type Params = ();
fn new(_client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
Ok(CustomSignedExtension)
Ok(CustomTransactionExtension)
}
}
// Encode whatever the extension needs to provide when asked:
impl ExtrinsicParamsEncoder for CustomSignedExtension {
fn encode_extra_to(&self, v: &mut Vec<u8>) {
impl ExtrinsicParamsEncoder for CustomTransactionExtension {
fn encode_value_to(&self, v: &mut Vec<u8>) {
"Hello".encode_to(v);
}
fn encode_additional_to(&self, v: &mut Vec<u8>) {
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
true.encode_to(v)
}
}
@@ -84,8 +85,8 @@ impl ExtrinsicParamsEncoder for CustomSignedExtension {
pub fn custom(
params: DefaultExtrinsicParamsBuilder<CustomConfig>,
) -> <<CustomConfig as Config>::ExtrinsicParams as ExtrinsicParams<CustomConfig>>::Params {
let (a, b, c, d, e, f, g, h) = params.build();
(a, b, c, d, e, f, g, h, ())
let (a, b, c, d, e, f, g, h, i) = params.build();
(a, b, c, d, e, f, g, h, i, ())
}
#[tokio::main]
@@ -69,10 +69,6 @@ mod pair_signer {
self.account_id.clone()
}
fn address(&self) -> <PolkadotConfig as Config>::Address {
self.account_id.clone().into()
}
fn sign(&self, signer_payload: &[u8]) -> <PolkadotConfig as Config>::Signature {
let signature = self.signer.sign(signer_payload);
MultiSignature::Sr25519(signature.0)
+5 -5
View File
@@ -10,7 +10,7 @@ pub mod polkadot {}
#[tokio::main]
async fn main() -> Result<(), BoxedError> {
// Spawned tasks require things held across await points to impl Send,
// so we use one to demonstrate that this is possible with `PartialExtrinsic`
// so we use one to demonstrate that this is possible with `PartialTransaction`
tokio::spawn(signing_example()).await??;
Ok(())
}
@@ -25,9 +25,9 @@ async fn signing_example() -> Result<(), BoxedError> {
let alice = dev::alice();
// Create partial tx, ready to be signed.
let partial_tx = api
let mut partial_tx = api
.tx()
.create_partial_signed(
.create_partial(
&balance_transfer_tx,
&alice.public_key().to_account_id(),
Default::default(),
@@ -35,13 +35,13 @@ async fn signing_example() -> Result<(), BoxedError> {
.await?;
// Simulate taking some time to get a signature back, in part to
// show that the `PartialExtrinsic` can be held across await points.
// show that the `PartialTransaction` can be held across await points.
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let signature = alice.sign(&partial_tx.signer_payload());
// Sign the transaction.
let tx = partial_tx
.sign_with_address_and_signature(&alice.public_key().to_address(), &signature.into());
.sign_with_account_and_signature(&alice.public_key().to_account_id(), &signature.into());
// Submit it.
tx.submit_and_watch()
+1 -6
View File
@@ -15,14 +15,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let dest = dev::bob().public_key().into();
let tx = polkadot::tx().balances().transfer_allow_death(dest, 10_000);
let latest_block = api.blocks().at_latest().await?;
// Configure the transaction parameters; we give a small tip and set the
// transaction to live for 32 blocks from the `latest_block` above.
let tx_params = Params::new()
.tip(1_000)
.mortal(latest_block.header(), 32)
.build();
let tx_params = Params::new().tip(1_000).mortal(32).build();
// submit the transaction:
let from = dev::alice();