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
@@ -12,7 +12,7 @@ use crate::utils::node_runtime;
#[cfg(fullclient)]
use subxt::{
config::{
signed_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce},
transaction_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce},
DefaultExtrinsicParamsBuilder, SubstrateConfig,
},
utils::Era,
@@ -262,7 +262,7 @@ async fn fetch_block_and_decode_extrinsic_details() {
#[cfg(fullclient)]
#[subxt_test]
async fn decode_signed_extensions_from_blocks() {
async fn decode_transaction_extensions_from_blocks() {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
@@ -301,7 +301,7 @@ async fn decode_signed_extensions_from_blocks() {
}
let transaction1 = submit_transfer_extrinsic_and_get_it_back!(1234);
let extensions1 = transaction1.signed_extensions().unwrap();
let extensions1 = transaction1.transaction_extensions().unwrap();
let nonce1 = extensions1.nonce().unwrap();
let nonce1_static = extensions1.find::<CheckNonce>().unwrap().unwrap();
@@ -313,7 +313,7 @@ async fn decode_signed_extensions_from_blocks() {
.tip();
let transaction2 = submit_transfer_extrinsic_and_get_it_back!(5678);
let extensions2 = transaction2.signed_extensions().unwrap();
let extensions2 = transaction2.transaction_extensions().unwrap();
let nonce2 = extensions2.nonce().unwrap();
let nonce2_static = extensions2.find::<CheckNonce>().unwrap().unwrap();
let tip2 = extensions2.tip().unwrap();
@@ -332,7 +332,7 @@ async fn decode_signed_extensions_from_blocks() {
assert_eq!(tip2, 5678);
assert_eq!(tip2, tip2_static);
let expected_signed_extensions = [
let expected_transaction_extensions = [
"CheckNonZeroSender",
"CheckSpecVersion",
"CheckTxVersion",
@@ -345,13 +345,25 @@ async fn decode_signed_extensions_from_blocks() {
"WeightReclaim",
];
assert_eq!(extensions1.iter().count(), expected_signed_extensions.len());
for (e, expected_name) in extensions1.iter().zip(expected_signed_extensions.iter()) {
assert_eq!(
extensions1.iter().count(),
expected_transaction_extensions.len()
);
for (e, expected_name) in extensions1
.iter()
.zip(expected_transaction_extensions.iter())
{
assert_eq!(e.name(), *expected_name);
}
assert_eq!(extensions2.iter().count(), expected_signed_extensions.len());
for (e, expected_name) in extensions2.iter().zip(expected_signed_extensions.iter()) {
assert_eq!(
extensions2.iter().count(),
expected_transaction_extensions.len()
);
for (e, expected_name) in extensions2
.iter()
.zip(expected_transaction_extensions.iter())
{
assert_eq!(e.name(), *expected_name);
}
@@ -271,8 +271,9 @@ async fn transactionwatch_v1_submit_and_watch() {
let tx_bytes = ctx
.client()
.tx()
.create_signed_offline(&payload, &dev::alice(), Default::default())
.create_partial_offline(&payload, Default::default())
.unwrap()
.sign(&dev::alice())
.into_encoded();
// Test submitting it:
@@ -337,8 +338,9 @@ async fn transaction_v1_broadcast() {
let tx = ctx
.client()
.tx()
.create_signed_offline(&tx_payload, &dev::alice(), Default::default())
.unwrap();
.create_partial_offline(&tx_payload, Default::default())
.unwrap()
.sign(&dev::alice());
let tx_hash = tx.hash();
let tx_bytes = tx.into_encoded();
@@ -407,8 +409,9 @@ async fn transaction_v1_stop() {
let tx_bytes = ctx
.client()
.tx()
.create_signed_offline(&tx, &dev::alice(), Default::default())
.create_partial_offline(&tx, Default::default())
.unwrap()
.sign(&dev::alice())
.into_encoded();
// Submit the transaction.
@@ -190,9 +190,9 @@ async fn external_signing() {
// Create a partial extrinsic. We can get the signer payload at this point, to be
// signed externally.
let tx = node_runtime::tx().preimage().note_preimage(vec![0u8]);
let partial_extrinsic = api
let mut partial_extrinsic = api
.tx()
.create_partial_signed(&tx, &alice.public_key().into(), Default::default())
.create_partial(&tx, &alice.public_key().into(), Default::default())
.await
.unwrap();
@@ -202,7 +202,7 @@ async fn external_signing() {
let signature = alice.sign(&signer_payload);
// Use this to build a signed extrinsic.
let extrinsic = partial_extrinsic
.sign_with_address_and_signature(&alice.public_key().into(), &signature.into());
.sign_with_account_and_signature(&alice.public_key().into(), &signature.into());
// And now submit it.
extrinsic
@@ -1,5 +0,0 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
mod validation;
@@ -6,6 +6,7 @@ mod blocks;
mod client;
mod codegen;
mod frame;
mod metadata;
mod metadata_validation;
mod runtime_api;
mod storage;
mod transactions;
@@ -0,0 +1,149 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::utils::node_runtime;
use crate::{subxt_test, test_context};
use core::ops::Deref;
use frame_decode::extrinsics::ExtrinsicType;
use subxt_signer::sr25519::dev;
// TODO: When VerifySignature exists on the substrate kitchensink runtime,
// let's try actuallty submitting v4 and v5 signed extrinsics to verify that
// they are actually accepted by the node.
#[subxt_test]
async fn v4_unsigned_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api.tx().create_v4_unsigned(&call).unwrap().into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded = frame_decode::extrinsics::decode_extrinsic(
tx_bytes_cursor,
md.deref(),
api.metadata().types(),
)
.unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 4);
assert_eq!(decoded.ty(), ExtrinsicType::Bare);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.signature_payload().is_none());
Ok(())
}
#[subxt_test]
async fn v5_bare_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api.tx().create_v5_bare(&call).unwrap().into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded = frame_decode::extrinsics::decode_extrinsic(
tx_bytes_cursor,
md.deref(),
api.metadata().types(),
)
.unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 5);
assert_eq!(decoded.ty(), ExtrinsicType::Bare);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.transaction_extension_payload().is_none());
assert!(decoded.signature_payload().is_none());
Ok(())
}
#[subxt_test]
async fn v4_signed_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api
.tx()
.create_v4_partial(&call, &dev::alice().public_key().into(), Default::default())
.await
.unwrap()
.sign(&dev::alice())
.into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded = frame_decode::extrinsics::decode_extrinsic(
tx_bytes_cursor,
md.deref(),
api.metadata().types(),
)
.unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 4);
assert_eq!(decoded.ty(), ExtrinsicType::Signed);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.signature_payload().is_some());
Ok(())
}
#[subxt_test]
async fn v5_general_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let dummy_signer = dev::alice();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api
.tx()
.create_v5_partial(&call, &dev::alice().public_key().into(), Default::default())
.await
.unwrap()
.sign(&dummy_signer) // No signature payload is added, but may be inserted into tx extensions.
.into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded = frame_decode::extrinsics::decode_extrinsic(
tx_bytes_cursor,
md.deref(),
api.metadata().types(),
)
.unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 5);
assert_eq!(decoded.ty(), ExtrinsicType::General);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.transaction_extension_payload().is_some());
// v5 general extrinsics have no signature payload; signature in tx extensions:
assert!(decoded.signature_payload().is_none());
Ok(())
}