From 1097f525d7dd16052d7d12e458006e40ad0a8d1b Mon Sep 17 00:00:00 2001 From: James Wilson Date: Mon, 9 Feb 2026 16:57:54 +0000 Subject: [PATCH 01/12] First draft adding extrinsic encoding methods to frame-decode --- src/methods/extrinsic_decoder.rs | 2 +- src/methods/extrinsic_encoder.rs | 685 ++++++++++++++++++ .../transaction_extension.rs | 65 ++ .../transaction_extensions.rs | 335 +++++++++ src/methods/extrinsic_type_info.rs | 78 +- src/methods/mod.rs | 1 + 6 files changed, 1141 insertions(+), 25 deletions(-) create mode 100644 src/methods/extrinsic_encoder.rs create mode 100644 src/methods/extrinsic_encoder/transaction_extension.rs create mode 100644 src/methods/extrinsic_encoder/transaction_extensions.rs diff --git a/src/methods/extrinsic_decoder.rs b/src/methods/extrinsic_decoder.rs index 391be28..c068d6b 100644 --- a/src/methods/extrinsic_decoder.rs +++ b/src/methods/extrinsic_decoder.rs @@ -619,7 +619,7 @@ where let call_index: u8 = Decode::decode(cursor).map_err(ExtrinsicDecodeError::CannotDecodeCallIndex)?; let call_info = info - .extrinsic_call_info(pallet_index, call_index) + .extrinsic_call_info_by_index(pallet_index, call_index) .map_err(|e| ExtrinsicDecodeError::CannotGetInfo(e.into_owned()))?; let mut call_data = vec![]; diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs new file mode 100644 index 0000000..18a53b8 --- /dev/null +++ b/src/methods/extrinsic_encoder.rs @@ -0,0 +1,685 @@ +// Copyright (C) 2022-2026 Parity Technologies (UK) Ltd. (admin@parity.io) +// This file is a part of the frame-decode crate. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod transaction_extensions; +mod transaction_extension; + +use super::extrinsic_type_info::{ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicSignatureInfo, ExtrinsicTypeInfo, ExtrinsicInfoError}; +use scale_encode::{EncodeAsType, EncodeAsFields}; +use scale_type_resolver::{TypeResolver, Field}; +use parity_scale_codec::Encode; + +pub use transaction_extension::{ + TransactionExtension, + TransactionExtensionError, +}; +pub use transaction_extensions::{ + TransactionExtensions, + TransactionExtensionsError, +}; + +/// An error returned trying to encode extrinsic call data. +#[non_exhaustive] +#[allow(missing_docs)] +#[derive(Debug, thiserror::Error)] +pub enum ExtrinsicEncodeError { + #[error("Cannot get extrinsic info: {0}")] + CannotGetInfo(ExtrinsicInfoError<'static>), + #[error("Extrinsic encoding failed: cannot encode call data: {0}")] + CannotEncodeCallData(scale_encode::Error), + #[error("Extrinsic encoding failed: cannot encode address: {0}")] + CannotEncodeAddress(scale_encode::Error), + #[error("Extrinsic encoding failed: cannot encode signature: {0}")] + CannotEncodeSignature(scale_encode::Error), + #[error("Extrinsic encoding failed: cannot encode transaction extensions: {0}")] + TransactionExtensions(TransactionExtensionsError), + #[error("Extrinsic encoding failed: cannot find a transaction extension version which enough data was given for.")] + CannotFindGoodExtensionVersion +} + +/// This is used when it is possible to provide some type +pub enum BytesOr<'a, T> { + Bytes(&'a [u8]), + Value(&'a T) +} + +//// V4 + +pub fn encode_v4_unsigned( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + info: &Info, + type_resolver: &Resolver, +) -> Result, ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, +{ + let mut out = Vec::new(); + encode_v4_unsigned_to( + pallet_name, + call_name, + call_data, + info, + type_resolver, + &mut out, + )?; + Ok(out) +} + +pub fn encode_v4_unsigned_to( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + info: &Info, + type_resolver: &Resolver, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, +{ + let call_info = info + .extrinsic_call_info_by_name(pallet_name, call_name) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + encode_v4_unsigned_with_info_to( + call_data, + type_resolver, + &call_info, + out + ) +} + +pub fn encode_v4_unsigned_with_info_to( + call_data: &CallData, + type_resolver: &Resolver, + call_info: &ExtrinsicCallInfo, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, +{ + encode_unsigned_at_version_with_info_to( + call_data, + &call_info, + type_resolver, + TransactionVersion::V4, + out + ) +} + +pub fn encode_v4_signed( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + transaction_extensions: &Exts, + address: &Address, + signature: &Signature, + info: &Info, + type_resolver: &Resolver, +) -> Result, ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, + Address: EncodeAsType, + Signature: EncodeAsType, +{ + let mut out = Vec::new(); + encode_v4_signed_to( + pallet_name, + call_name, + call_data, + transaction_extensions, + address, + signature, + info, + type_resolver, + &mut out, + )?; + Ok(out) +} + +pub fn encode_v4_signed_to( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + transaction_extensions: &Exts, + address: &Address, + signature: &Signature, + info: &Info, + type_resolver: &Resolver, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, + Address: EncodeAsType, + Signature: EncodeAsType, +{ + let call_info = info + .extrinsic_call_info_by_name(pallet_name, call_name) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + let ext_info = info + .extrinsic_extension_info(None) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + let sig_info = info + .extrinsic_signature_info() + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + encode_v4_signed_with_info_to( + call_data, + transaction_extensions, + address, + signature, + type_resolver, + &call_info, + &sig_info, + &ext_info, + out, + ) +} + +pub fn encode_v4_signed_with_info_to( + call_data: &CallData, + transaction_extensions: &Exts, + address: &Address, + signature: &Signature, + type_resolver: &Resolver, + call_info: &ExtrinsicCallInfo, + sig_info: &ExtrinsicSignatureInfo, + ext_info: &ExtrinsicExtensionInfo, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Exts: TransactionExtensions, + Address: EncodeAsType, + Signature: EncodeAsType, +{ + // Encode the "inner" bytes + let mut encoded_inner = Vec::new(); + // "is signed" + transaction protocol version (4) + (0b10000000 + 4u8).encode_to(&mut encoded_inner); + // Who is this transaction from (corresponds to public key of signature) + address + .encode_as_type_to(sig_info.address_id.clone(), type_resolver, &mut encoded_inner) + .map_err(ExtrinsicEncodeError::CannotEncodeAddress)?; + // Signature for the above identity + signature + .encode_as_type_to(sig_info.signature_id.clone(), type_resolver, &mut encoded_inner) + .map_err(ExtrinsicEncodeError::CannotEncodeSignature)?; + // Signed extensions (now Transaction Extensions) + for ext in &ext_info.extension_ids { + transaction_extensions + .encode_extension_value_to(&ext.name, ext.id.clone(), type_resolver, &mut encoded_inner) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } + // And now the actual call data, ie the arguments we're passing to the call + encode_call_data_with_info_to( + call_data, + call_info, + type_resolver, + &mut encoded_inner + ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + + // Now, encoding these inner bytes prefixes the compact length to the beginning: + encoded_inner.encode_to(out); + Ok(()) +} + +pub fn encode_v4_signer_payload( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + transaction_extensions: &Exts, + info: &Info, + type_resolver: &Resolver, +) -> Result, ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, + Info::TypeId: Clone, +{ + let call_info = info + .extrinsic_call_info_by_name(pallet_name, call_name) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + let ext_info = info + .extrinsic_extension_info(None) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + encode_v4_signer_payload_with_info( + call_data, + transaction_extensions, + type_resolver, + &call_info, + &ext_info, + ) +} + +pub fn encode_v4_signer_payload_with_info( + call_data: &CallData, + transaction_extensions: &Exts, + type_resolver: &Resolver, + call_info: &ExtrinsicCallInfo, + ext_info: &ExtrinsicExtensionInfo, +) -> Result, ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Exts: TransactionExtensions, +{ + let mut out = Vec::new(); + + // First encode call data + encode_call_data_with_info_to( + call_data, + &call_info, + type_resolver, + &mut out + ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + // Then the signer payload value (ie roughly the bytes that will appear in the tx) + for ext in &ext_info.extension_ids { + transaction_extensions + .encode_extension_value_for_signer_payload_to(&ext.name, ext.id.clone(), type_resolver, &mut out) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } + // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) + for ext in &ext_info.extension_ids { + transaction_extensions + .encode_extension_implicit_to(&ext.name, ext.id.clone(), type_resolver, &mut out) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } + // Finally we need to hash it if it's too long + if out.len() > 256 { + out = sp_crypto_hashing::blake2_256(&out).to_vec(); + } + + Ok(out) +} + +//// V5 + +pub fn encode_v5_bare( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + info: &Info, + type_resolver: &Resolver, +) -> Result, ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, +{ + let mut out = Vec::new(); + encode_v5_bare_to( + pallet_name, + call_name, + call_data, + info, + type_resolver, + &mut out, + )?; + Ok(out) +} + +pub fn encode_v5_bare_to( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + info: &Info, + type_resolver: &Resolver, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, +{ + let call_info = info + .extrinsic_call_info_by_name(pallet_name, call_name) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + encode_v5_bare_with_info_to( + call_data, + type_resolver, + &call_info, + out + ) +} + +pub fn encode_v5_bare_with_info_to( + call_data: &CallData, + type_resolver: &Resolver, + call_info: &ExtrinsicCallInfo, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, +{ + encode_unsigned_at_version_with_info_to( + call_data, + &call_info, + type_resolver, + TransactionVersion::V5, + out + ) +} + +pub fn best_v5_general_transaction_extension_version( + transaction_extensions: &Exts, + info: &Info, +) -> Result +where + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, + Resolver: TypeResolver, +{ + let extension_versions = info + .extrinsic_extension_version_info() + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + for ext_version in extension_versions { + // get extension info for each version. + let ext_info = info + .extrinsic_extension_info(Some(ext_version)) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + // Do we have all of the extension data for this version? + let have_data = ext_info.extension_ids.iter().all(|e| { + transaction_extensions.contains_extension(&e.name) + }); + + // If we have all of the data we need, encode the extrinsic, + // else loop and try the next extension version. + if have_data { + return Ok(ext_version) + } + } + + Err(ExtrinsicEncodeError::CannotFindGoodExtensionVersion) +} + +pub fn encode_v5_general( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + transaction_extension_version: u8, + transaction_extensions: &Exts, + info: &Info, + type_resolver: &Resolver, +) -> Result, ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, +{ + let mut out = Vec::new(); + encode_v5_general_to( + pallet_name, + call_name, + call_data, + transaction_extension_version, + transaction_extensions, + info, + type_resolver, + &mut out, + )?; + Ok(out) +} + +pub fn encode_v5_general_to( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + transaction_extension_version: u8, + transaction_extensions: &Exts, + info: &Info, + type_resolver: &Resolver, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, +{ + let call_info = info + .extrinsic_call_info_by_name(pallet_name, call_name) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + let ext_info = info + .extrinsic_extension_info(Some(transaction_extension_version)) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + return encode_v5_general_with_info_to( + call_data, + transaction_extension_version, + transaction_extensions, + type_resolver, + &call_info, + &ext_info, + out, + ) +} + +pub fn encode_v5_general_with_info_to( + call_data: &CallData, + transaction_extension_version: u8, + transaction_extensions: &Exts, + type_resolver: &Resolver, + call_info: &ExtrinsicCallInfo, + ext_info: &ExtrinsicExtensionInfo, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Exts: TransactionExtensions, +{ + // Encode the "inner" bytes + let mut encoded_inner = Vec::new(); + // "is signed" + transaction protocol version (4) + (0b01000000 + 5u8).encode_to(&mut encoded_inner); + // Version of the transaction extensions. + transaction_extension_version.encode_to(&mut encoded_inner); + // Transaction Extensions next. These may include a signature/address. + for ext in &ext_info.extension_ids { + transaction_extensions + .encode_extension_value_to(&ext.name, ext.id.clone(), type_resolver, &mut encoded_inner) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } + // And now the actual call data, ie the arguments we're passing to the call + encode_call_data_with_info_to( + call_data, + call_info, + type_resolver, + &mut encoded_inner + ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + + // Now, encoding these inner bytes prefixes the compact length to the beginning: + encoded_inner.encode_to(out); + Ok(()) +} + +pub fn encode_v5_signer_payload( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + transaction_extension_version: u8, + transaction_extensions: &Exts, + info: &Info, + type_resolver: &Resolver, +) -> Result<[u8; 32], ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, + Info::TypeId: Clone, +{ + let call_info = info + .extrinsic_call_info_by_name(pallet_name, call_name) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + let ext_info = info + .extrinsic_extension_info(Some(transaction_extension_version)) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + encode_v5_signer_payload_with_info( + call_data, + transaction_extensions, + type_resolver, + &call_info, + &ext_info, + ) +} + +pub fn encode_v5_signer_payload_with_info( + call_data: &CallData, + transaction_extensions: &Exts, + type_resolver: &Resolver, + call_info: &ExtrinsicCallInfo, + ext_info: &ExtrinsicExtensionInfo, +) -> Result<[u8; 32], ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Exts: TransactionExtensions, +{ + let mut out = Vec::new(); + + // First encode call data + encode_call_data_with_info_to( + call_data, + &call_info, + type_resolver, + &mut out + ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + + // Then the signer payload value (ie roughly the bytes that will appear in the tx) + for ext in &ext_info.extension_ids { + transaction_extensions + .encode_extension_value_for_signer_payload_to(&ext.name, ext.id.clone(), type_resolver, &mut out) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } + + // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) + for ext in &ext_info.extension_ids { + transaction_extensions + .encode_extension_implicit_to(&ext.name, ext.id.clone(), type_resolver, &mut out) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } + + // Finally hash it (regardless of length). + Ok(sp_crypto_hashing::blake2_256(&out)) +} + +// V4 unsigned and V5 bare extrinsics are basically encoded +// in the same way; this helper can do either. +fn encode_unsigned_at_version_with_info_to( + call_data: &CallData, + call_info: &ExtrinsicCallInfo, + type_resolver: &Resolver, + tx_version: TransactionVersion, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + Resolver: TypeResolver, + CallData: EncodeAsFields, +{ + // Build our inner, non-length-prefixed extrinsic: + let inner = { + let mut out = Vec::new(); + // Transaction version (4): + (tx_version as u8).encode_to(&mut out); + // Pallet and call index to identify the call: + call_info.pallet_index.encode_to(&mut out); + call_info.call_index.encode_to(&mut out); + // Then the arguments for the call: + encode_call_data_with_info_to( + call_data, + call_info, + type_resolver, + &mut out + ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + out + }; + + // Encode the inner vec to prefix the compact length to it: + inner.encode_to(out); + Ok(()) +} + +#[derive(Copy, Clone)] +#[repr(u8)] +enum TransactionVersion { + V4 = 4u8, + V5 = 5u8, +} + +// Encoding call data is the same for any extrinsic; this method does this part. +fn encode_call_data_with_info_to( + call_data: &CallData, + call_info: &ExtrinsicCallInfo, + type_resolver: &Resolver, + out: &mut Vec, +) -> Result<(), scale_encode::Error> +where + Resolver: TypeResolver, + CallData: EncodeAsFields, +{ + let mut fields = call_info.args.iter().map(|arg| { + Field { + name: Some(&*arg.name), + id: arg.id.clone() + } + }); + + call_data + .encode_as_fields_to(&mut fields, type_resolver, out)?; + + Ok(()) +} \ No newline at end of file diff --git a/src/methods/extrinsic_encoder/transaction_extension.rs b/src/methods/extrinsic_encoder/transaction_extension.rs new file mode 100644 index 0000000..51bb1aa --- /dev/null +++ b/src/methods/extrinsic_encoder/transaction_extension.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2022-2026 Parity Technologies (UK) Ltd. (admin@parity.io) +// This file is a part of the frame-decode crate. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use scale_type_resolver::TypeResolver; + +/// This can be implemented for anything which is a valid Substrate transaction extension. +/// Transaction extensions each have a unique name to identify them, and are able to encode +/// explicit `value` bytes to a transaction, or "implicit" bytes to a transaction signer payload. +pub trait TransactionExtension { + /// The name of this transaction extension. + fn extension_name(&self) -> &'static str; + + /// Given type information for the expected transaction extension, + /// this should encode the value (ie the bytes that will appear in the + /// transaction) to the provided `Vec`, or encode nothing and emit an error. + fn encode_value_to( + &self, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionError>; + + /// Given type information for the expected transaction extension, + /// this should encode the value that will be signed as a part of the + /// signer payload. + /// + /// This defaults to calling [`Self::encode_value_to`] if not implemented. + /// In most cases this is fine, but for V5 extrinsics we can optionally provide + /// the signature inside a transaction extension, and so that transaction would be + /// unable to encode anything for the signer payload and thus should override this + /// method to encode nothing. + fn encode_value_for_signer_payload_to( + &self, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionError> { + self.encode_value_to(type_id, type_resolver, out) + } + + /// Given type information for the expected transaction extension, + /// this should encode the implicit (ie the bytes that will appear in the + /// signer payload) to the provided `Vec`, or encode nothing and emit an error. + fn encode_implicit_to( + &self, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionError>; +} + +/// This error will be returned if any of the methods in [`TransactionExtension`] fail. +pub type TransactionExtensionError = Box; diff --git a/src/methods/extrinsic_encoder/transaction_extensions.rs b/src/methods/extrinsic_encoder/transaction_extensions.rs new file mode 100644 index 0000000..96ca38a --- /dev/null +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -0,0 +1,335 @@ +// Copyright (C) 2022-2026 Parity Technologies (UK) Ltd. (admin@parity.io) +// This file is a part of the frame-decode crate. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use scale_type_resolver::TypeResolver; +use super::transaction_extension::{ TransactionExtension, TransactionExtensionError }; + +/// This trait can be implemented for anything which represents a set of transaction extensions. +/// It's implemented by default for tuples of items which implement [`TransactionExtension`], +/// and for slices of `&dyn TransactionExtension`. +pub trait TransactionExtensions { + /// Is a given transaction extension contained within this set? + fn contains_extension(&self, name: &str) -> bool; + + /// This will be called given the name of each transaction extension we + /// wish to obtain the encoded bytes to. Implementations are expected to + /// write the bytes that should be included in the **transaction** to the given [`Vec`], + /// or return an error if no such bytes can be written. + fn encode_extension_value_to( + &self, + name: &str, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError>; + + /// This will be called given the name of each transaction extension we + /// wish to obtain the encoded bytes to. Implementations are expected to + /// write the bytes that should be included in the **signer payload value + /// section** to the given [`Vec`], or return an error if no such bytes can be + /// written. + /// + /// This defaults to calling [`Self::encode_extension_value_to`] if not implemented. + /// In most cases this is fine, but for V5 extrinsics we can optionally provide + /// the signature inside a transaction extension, and so that transaction would be + /// unable to encode anything for the signer payload. + fn encode_extension_value_for_signer_payload_to( + &self, + name: &str, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + self.encode_extension_value_to(name, type_id, type_resolver, out) + } + + /// This will be called given the name of each transaction extension we + /// wish to obtain the encoded bytes to. Implementations are expected to + /// write the bytes that should be included in the **signer payload implicit** + /// to the given [`Vec`], or return an error if no such bytes can be written. + fn encode_extension_implicit_to( + &self, + name: &str, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError>; +} + +/// This error will be returned if any of the methods in [`TransactionExtensions`] fail. +#[derive(Debug, thiserror::Error)] +pub enum TransactionExtensionsError { + #[error("Cannot encode transaction extension '{0}': This extension could not be found")] + NotFound(String), + #[error("Cannot encode transaction extension '{extension_name}': {error}")] + Other { + extension_name: String, + error: TransactionExtensionError, + } +} + +// `TransactionExtension` is object safe and so `TransactionExtensions` can be implemented +// for slices of `&dyn TransactionExtension`s. +impl TransactionExtensions for [&dyn TransactionExtension] { + fn contains_extension(&self, name: &str) -> bool { + self.iter().find(|e| e.extension_name() == name).is_some() + } + + fn encode_extension_value_to( + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + let len = out.len(); + for ext in self { + if ext.extension_name() == name { + return ext.encode_value_to(type_id, type_resolver, out) + .map_err(|e| { + // Protection: if we are returning an error then + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + while out.len() > len { + out.pop(); + } + TransactionExtensionsError::Other { + extension_name: name.to_owned(), + error: e, + } + }); + } + } + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } + + fn encode_extension_value_for_signer_payload_to( + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + let len = out.len(); + for ext in self { + if ext.extension_name() == name { + return ext.encode_value_for_signer_payload_to(type_id, type_resolver, out) + .map_err(|e| { + // Protection: if we are returning an error then + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + while out.len() > len { + out.pop(); + } + TransactionExtensionsError::Other { + extension_name: name.to_owned(), + error: e, + } + }); + } + } + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } + + fn encode_extension_implicit_to( + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + let len = out.len(); + for ext in self { + if ext.extension_name() == name { + return ext.encode_implicit_to(type_id, type_resolver, out) + .map_err(|e| { + // Protection: if we are returning an error then + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + while out.len() > len { + out.pop(); + } + TransactionExtensionsError::Other { + extension_name: name.to_owned(), + error: e, + } + }); + } + } + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } +} + +// Empty tuples impl `TransactionExtensions`: if called they emit a not found error. +impl TransactionExtensions for () { + fn contains_extension(&self, _name: &str) -> bool { + false + } + + fn encode_extension_value_to( + &self, + name: &str, + _type_id: ::TypeId, + _type_resolver: &Resolver, + _out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } + + fn encode_extension_implicit_to( + &self, + name: &str, + _type_id: ::TypeId, + _type_resolver: &Resolver, + _out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } +} + +// Non-empty tuples impl `TransactionExtensions`: for each extension we do a linear +// search through the tuple items to find it and call the appropriate encode method. +macro_rules! impl_tuples { + ($($ident:ident $index:tt),*) => { + impl TransactionExtensions for ($($ident,)*) + where + $($ident: TransactionExtension,)* + { + fn contains_extension(&self, name: &str) -> bool { + $( + if self.$index.extension_name() == name { + return true + } + )* + false + } + + fn encode_extension_value_to( + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + let len = out.len(); + + $( + if self.$index.extension_name() == name { + return self.$index.encode_value_to(type_id, type_resolver, out) + .map_err(|e| { + // Protection: if we are returning an error then + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + while out.len() > len { + out.pop(); + } + TransactionExtensionsError::Other { + extension_name: name.to_owned(), + error: e, + } + }); + } + )* + + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } + + fn encode_extension_value_for_signer_payload_to( + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + let len = out.len(); + + $( + if self.$index.extension_name() == name { + return self.$index.encode_value_for_signer_payload_to(type_id, type_resolver, out) + .map_err(|e| { + // Protection: if we are returning an error then + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + while out.len() > len { + out.pop(); + } + TransactionExtensionsError::Other { + extension_name: name.to_owned(), + error: e, + } + }); + } + )* + + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } + + fn encode_extension_implicit_to( + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec + ) -> Result<(), TransactionExtensionsError> { + let len = out.len(); + + $( + if self.$index.extension_name() == name { + return self.$index.encode_implicit_to(type_id, type_resolver, out) + .map_err(|e| { + // Protection: if we are returning an error then + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + while out.len() > len { + out.pop(); + } + TransactionExtensionsError::Other { + extension_name: name.to_owned(), + error: e, + } + }); + } + )* + + Err(TransactionExtensionsError::NotFound(name.to_owned())) + } + } + } +} + +#[rustfmt::skip] +const _: () = { + impl_tuples!(A 0); + impl_tuples!(A 0, B 1); + impl_tuples!(A 0, B 1, C 2); + impl_tuples!(A 0, B 1, C 2, D 3); + impl_tuples!(A 0, B 1, C 2, D 3, E 4); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19, V 20); +}; diff --git a/src/methods/extrinsic_type_info.rs b/src/methods/extrinsic_type_info.rs index b24f869..323c0e9 100644 --- a/src/methods/extrinsic_type_info.rs +++ b/src/methods/extrinsic_type_info.rs @@ -14,33 +14,59 @@ // limitations under the License. use alloc::borrow::Cow; -use alloc::string::String; +//use alloc::string::String; use alloc::vec::Vec; use core::write; -#[cfg(feature = "legacy")] -use {crate::utils::as_decoded, scale_info_legacy::LookupName}; +// #[cfg(feature = "legacy")] +// use {crate::utils::as_decoded, scale_info_legacy::LookupName}; -/// This is implemented for all metadatas exposed from `frame_metadata` and is responsible for extracting the -/// type IDs that we need in order to decode extrinsics. +/// Implementations of this are responsible for handing back the information we need to +/// encode and decode extrinsics. This is expected to be implemented for runtime metadata +/// types or derivatives thereof where needed. pub trait ExtrinsicTypeInfo { /// The type of type IDs that we are using to obtain type information. type TypeId; - /// Get the information about the call data of a given extrinsic. - fn extrinsic_call_info( + + /// Get the information about the call data of a given extrinsic, given u8 indexes. + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, ) -> Result, ExtrinsicInfoError<'_>>; - /// Get the information needed to decode the extrinsic signature bytes. + + /// Get the information about the call data of a given extrinsic, given pallet and call name. + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>>; + + /// Get the information needed to decode or encode the extrinsic signature bytes. fn extrinsic_signature_info( &self, ) -> Result, ExtrinsicInfoError<'_>>; + /// Get the information needed to decode the transaction extensions. fn extrinsic_extension_info( &self, extension_version: Option, ) -> Result, ExtrinsicInfoError<'_>>; + + /// Get the available transaction extension versions. Prior to runtimes supporting + /// V5 extrinsics this should not return any entries. In runtimes supporting V5 extrinsics, + /// 1 or more versions should be returned. + /// + /// Versions should be returned in order from highest to lowest preference of + /// whether they should be used for encoding extrinsics. The highest will be tried first, + /// but if the relevant transaction extension information is not given then we will fall + /// back to the next version until we find one we can satisfy. + /// + /// All versions returned here should be passable to [`ExtrinsicTypeInfo::extrinsic_extension_info`] + /// and return valid extensions information from that. + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>>; } /// An error returned trying to access extrinsic type information. @@ -223,9 +249,13 @@ pub struct ExtrinsicInfoArg<'info, TypeId> { pub id: TypeId, } -/// Extrinsic call data information. +/// Extrinsic call data information given pallet and call names. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtrinsicCallInfo<'info, TypeId> { + /// Index of the pallet. + pub pallet_index: u8, + /// Index of the call. + pub call_index: u8, /// Name of the pallet. pub pallet_name: Cow<'info, str>, /// Name of the call. @@ -318,7 +348,7 @@ macro_rules! impl_call_arg_ids_body_for_v14_to_v16 { }) .collect(); - Ok(ExtrinsicCallInfo { + Ok(ExtrinsicCallInfoByIndex { pallet_name: Cow::Borrowed(pallet_name), call_name: Cow::Borrowed(&call_variant.name), args, @@ -328,11 +358,11 @@ macro_rules! impl_call_arg_ids_body_for_v14_to_v16 { impl ExtrinsicTypeInfo for frame_metadata::v14::RuntimeMetadataV14 { type TypeId = u32; - fn extrinsic_call_info( + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { + ) -> Result, ExtrinsicInfoError<'_>> { impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) } fn extrinsic_signature_info( @@ -367,11 +397,11 @@ impl ExtrinsicTypeInfo for frame_metadata::v14::RuntimeMetadataV14 { impl ExtrinsicTypeInfo for frame_metadata::v15::RuntimeMetadataV15 { type TypeId = u32; - fn extrinsic_call_info( + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { + ) -> Result, ExtrinsicInfoError<'_>> { impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) } fn extrinsic_signature_info( @@ -404,11 +434,11 @@ impl ExtrinsicTypeInfo for frame_metadata::v15::RuntimeMetadataV15 { impl ExtrinsicTypeInfo for frame_metadata::v16::RuntimeMetadataV16 { type TypeId = u32; - fn extrinsic_call_info( + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { + ) -> Result, ExtrinsicInfoError<'_>> { impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) } fn extrinsic_signature_info( @@ -579,7 +609,7 @@ const _: () = { }) .collect::>()?; - Ok(ExtrinsicCallInfo { + Ok(ExtrinsicCallInfoByIndex { pallet_name: Cow::Borrowed(m_name), call_name: Cow::Borrowed(c_name), args, @@ -591,11 +621,11 @@ const _: () = { ($path:path) => { impl ExtrinsicTypeInfo for $path { type TypeId = LookupName; - fn extrinsic_call_info( + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { + ) -> Result, ExtrinsicInfoError<'_>> { impl_extrinsic_info_body_for_v8_to_v11!(self, pallet_index, call_index) } fn extrinsic_signature_info( @@ -629,11 +659,11 @@ const _: () = { impl ExtrinsicTypeInfo for frame_metadata::v11::RuntimeMetadataV11 { type TypeId = LookupName; - fn extrinsic_call_info( + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { + ) -> Result, ExtrinsicInfoError<'_>> { impl_extrinsic_info_body_for_v8_to_v11!(self, pallet_index, call_index) } fn extrinsic_signature_info( @@ -675,11 +705,11 @@ const _: () = { ($path:path) => { impl ExtrinsicTypeInfo for $path { type TypeId = LookupName; - fn extrinsic_call_info( + fn extrinsic_call_info_by_index( &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { + ) -> Result, ExtrinsicInfoError<'_>> { let modules = as_decoded(&self.modules); let m = modules @@ -729,7 +759,7 @@ const _: () = { }) .collect::>()?; - Ok(ExtrinsicCallInfo { + Ok(ExtrinsicCallInfoByIndex { pallet_name: Cow::Borrowed(m_name), call_name: Cow::Borrowed(c_name), args, diff --git a/src/methods/mod.rs b/src/methods/mod.rs index f5f9d24..86d11e4 100644 --- a/src/methods/mod.rs +++ b/src/methods/mod.rs @@ -18,6 +18,7 @@ pub mod constant_type_info; pub mod custom_value_decoder; pub mod custom_value_type_info; pub mod extrinsic_decoder; +pub mod extrinsic_encoder; pub mod extrinsic_type_info; pub mod runtime_api_decoder; pub mod runtime_api_encoder; From cb0d6dd1c214f07616a31d096deaf8122eec94cd Mon Sep 17 00:00:00 2001 From: James Wilson Date: Mon, 9 Feb 2026 17:36:45 +0000 Subject: [PATCH 02/12] Add exampels to extrinsic encode methods --- src/lib.rs | 13 + src/methods/extrinsic_encoder.rs | 467 +++++++++++++++++- .../transaction_extensions.rs | 4 + src/methods/extrinsic_type_info.rs | 366 +++++++++++++- 4 files changed, 816 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 04ae75c..c5925ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,19 @@ pub mod extrinsics { Extrinsic, ExtrinsicDecodeError, ExtrinsicExtensions, ExtrinsicOwned, ExtrinsicSignature, ExtrinsicType, NamedArg, decode_extrinsic, }; + pub use crate::methods::extrinsic_encoder::{ + TransactionExtension, + TransactionExtensionError, + TransactionExtensions, + TransactionExtensionsError, + ExtrinsicEncodeError, + encode_v4_signed, encode_v4_signed_to, + encode_v4_signed_with_info_to, encode_v4_signer_payload, encode_v4_signer_payload_with_info, + encode_v4_unsigned, encode_v4_unsigned_to, encode_v4_unsigned_with_info_to, encode_v5_bare, + encode_v5_bare_to, encode_v5_bare_with_info_to, encode_v5_general, encode_v5_general_to, + encode_v5_general_with_info_to, encode_v5_signer_payload, encode_v5_signer_payload_with_info, + best_v5_general_transaction_extension_version, + }; pub use crate::methods::extrinsic_type_info::{ ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicInfoArg, ExtrinsicInfoError, ExtrinsicSignatureInfo, ExtrinsicTypeInfo, diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index 18a53b8..748b1df 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -49,14 +49,37 @@ pub enum ExtrinsicEncodeError { CannotFindGoodExtensionVersion } -/// This is used when it is possible to provide some type -pub enum BytesOr<'a, T> { - Bytes(&'a [u8]), - Value(&'a T) -} - //// V4 +/// Encode a V4 unsigned extrinsic (also known as an inherent). +/// +/// This is the same as [`encode_v4_unsigned_to`], but returns the encoded extrinsic as a `Vec`, +/// rather than accepting a mutable output buffer. +/// +/// # Example +/// +/// ```rust +/// use frame_decode::extrinsics::encode_v4_unsigned; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata_10000000_9180.scale").unwrap(); +/// let RuntimeMetadata::V14(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// // Encode a call to Timestamp.set with an argument. +/// // The call_data type must implement `scale_encode::EncodeAsFields`. +/// let call_data = scale_value::value!({ +/// now: 1234567890u64, +/// }); +/// +/// let encoded = encode_v4_unsigned( +/// "Timestamp", +/// "set", +/// &call_data, +/// &metadata, +/// &metadata.types, +/// ).unwrap(); +/// ``` pub fn encode_v4_unsigned( pallet_name: &str, call_name: &str, @@ -81,6 +104,36 @@ where Ok(out) } +/// Encode a V4 unsigned extrinsic (also known as an inherent) to a provided output buffer. +/// +/// This is the same as [`encode_v4_unsigned`], but writes the encoded extrinsic to the provided +/// `Vec` rather than returning a new one. +/// +/// # Example +/// +/// ```rust +/// use frame_decode::extrinsics::encode_v4_unsigned_to; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata_10000000_9180.scale").unwrap(); +/// let RuntimeMetadata::V14(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// // Encode a call to Timestamp.set with an argument. +/// let call_data = scale_value::value!({ +/// now: 1234567890u64, +/// }); +/// +/// let mut encoded = Vec::new(); +/// encode_v4_unsigned_to( +/// "Timestamp", +/// "set", +/// &call_data, +/// &metadata, +/// &metadata.types, +/// &mut encoded, +/// ).unwrap(); +/// ``` pub fn encode_v4_unsigned_to( pallet_name: &str, call_name: &str, @@ -88,7 +141,7 @@ pub fn encode_v4_unsigned_to( info: &Info, type_resolver: &Resolver, out: &mut Vec, -) -> Result<(), ExtrinsicEncodeError> +) -> Result<(), ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -98,7 +151,7 @@ where .extrinsic_call_info_by_name(pallet_name, call_name) .map_err(|i| i.into_owned()) .map_err(ExtrinsicEncodeError::CannotGetInfo)?; - + encode_v4_unsigned_with_info_to( call_data, type_resolver, @@ -107,6 +160,12 @@ where ) } +/// Encode a V4 unsigned extrinsic (also known as an inherent) to a provided output buffer, +/// using pre-computed call information. +/// +/// Unlike [`encode_v4_unsigned_to`], which obtains the call info internally given the pallet and call names, +/// this function takes the call info as an argument. This is useful if you already have the call info available, +/// for example if you are encoding multiple extrinsics for the same call. pub fn encode_v4_unsigned_with_info_to( call_data: &CallData, type_resolver: &Resolver, @@ -116,7 +175,7 @@ pub fn encode_v4_unsigned_with_info_to( where CallData: EncodeAsFields, Resolver: TypeResolver, -{ +{ encode_unsigned_at_version_with_info_to( call_data, &call_info, @@ -126,6 +185,43 @@ where ) } +/// Encode a V4 signed extrinsic, ready to submit. +/// +/// A signed V4 extrinsic includes an address, signature, and transaction extensions (such as +/// nonce and tip) alongside the call data. The signature should be computed over the signer +/// payload, which can be obtained via [`encode_v4_signer_payload`]. +/// +/// This is the same as [`encode_v4_signed_to`], but returns the encoded extrinsic as a `Vec`, +/// rather than accepting a mutable output buffer. +/// +/// # Example +/// +/// ```rust,ignore +/// use frame_decode::extrinsics::{encode_v4_signed, TransactionExtensions}; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata_10000000_9180.scale").unwrap(); +/// let RuntimeMetadata::V14(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// // The call data, address, signature, and transaction extensions must implement +/// // the appropriate scale_encode traits. +/// let call_data = /* ... */; +/// let address = /* your address type */; +/// let signature = /* your signature type */; +/// let transaction_extensions = /* your TransactionExtensions impl */; +/// +/// let encoded = encode_v4_signed( +/// "Balances", +/// "transfer_keep_alive", +/// &call_data, +/// &transaction_extensions, +/// &address, +/// &signature, +/// &metadata, +/// &metadata.types, +/// ).unwrap(); +/// ``` pub fn encode_v4_signed( pallet_name: &str, call_name: &str, @@ -135,7 +231,7 @@ pub fn encode_v4_signed( signature: &Signature, info: &Info, type_resolver: &Resolver, -) -> Result, ExtrinsicEncodeError> +) -> Result, ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -159,6 +255,43 @@ where Ok(out) } +/// Encode a V4 signed extrinsic to a provided output buffer. +/// +/// A signed extrinsic includes an address, signature, and transaction extensions (such as +/// nonce and tip) alongside the call data. The signature should be computed over the signer +/// payload, which can be obtained via [`encode_v4_signer_payload`]. +/// +/// This is the same as [`encode_v4_signed`], but writes the encoded extrinsic to the provided +/// `Vec` rather than returning a new one. +/// +/// # Example +/// +/// ```rust,ignore +/// use frame_decode::extrinsics::{encode_v4_signed_to, TransactionExtensions}; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata_10000000_9180.scale").unwrap(); +/// let RuntimeMetadata::V14(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// let call_data = /* ... */; +/// let address = /* your address type */; +/// let signature = /* your signature type */; +/// let transaction_extensions = /* your TransactionExtensions impl */; +/// +/// let mut encoded = Vec::new(); +/// encode_v4_signed_to( +/// "Balances", +/// "transfer_keep_alive", +/// &call_data, +/// &transaction_extensions, +/// &address, +/// &signature, +/// &metadata, +/// &metadata.types, +/// &mut encoded, +/// ).unwrap(); +/// ``` pub fn encode_v4_signed_to( pallet_name: &str, call_name: &str, @@ -206,6 +339,11 @@ where ) } +/// Encode a V4 signed extrinsic to a provided output buffer, using pre-computed type information. +/// +/// Unlike [`encode_v4_signed_to`], which obtains the call, signature, and extension info internally +/// given the pallet and call names, this function takes these as arguments. This is useful if you +/// already have this information available, for example if you are encoding multiple extrinsics. pub fn encode_v4_signed_with_info_to( call_data: &CallData, transaction_extensions: &Exts, @@ -255,6 +393,42 @@ where Ok(()) } +/// Encode the signer payload for a V4 signed extrinsic. +/// +/// The signer payload is the data that should be signed to produce the signature for +/// a signed extrinsic. It consists of the encoded call data, the transaction extension +/// values, and the transaction extension implicit data. If the resulting payload exceeds +/// 256 bytes, it is hashed using Blake2-256. +/// +/// Use this function to obtain the bytes that should be signed, then pass the resulting +/// signature to [`encode_v4_signed`] to construct the final extrinsic. +/// +/// # Example +/// +/// ```rust,ignore +/// use frame_decode::extrinsics::{encode_v4_signer_payload, TransactionExtensions}; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata_10000000_9180.scale").unwrap(); +/// let RuntimeMetadata::V14(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// let call_data = /* ... */; +/// let transaction_extensions = /* your TransactionExtensions impl */; +/// +/// // Get the payload to sign +/// let signer_payload = encode_v4_signer_payload( +/// "Balances", +/// "transfer_keep_alive", +/// &call_data, +/// &transaction_extensions, +/// &metadata, +/// &metadata.types, +/// ).unwrap(); +/// +/// // Sign the payload with your signing key, then use encode_v4_signed +/// // to construct the final extrinsic. +/// ``` pub fn encode_v4_signer_payload( pallet_name: &str, call_name: &str, @@ -262,7 +436,7 @@ pub fn encode_v4_signer_payload( transaction_extensions: &Exts, info: &Info, type_resolver: &Resolver, -) -> Result, ExtrinsicEncodeError> +) -> Result, ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -289,13 +463,22 @@ where ) } +/// Encode the signer payload for a V4 signed extrinsic, using pre-computed type information. +/// +/// Unlike [`encode_v4_signer_payload`], which obtains the call and extension info internally +/// given the pallet and call names, this function takes these as arguments. This is useful if you +/// already have this information available. +/// +/// The signer payload consists of the encoded call data, the transaction extension values, +/// and the transaction extension implicit data. If the resulting payload exceeds 256 bytes, +/// it is hashed using Blake2-256. pub fn encode_v4_signer_payload_with_info( call_data: &CallData, transaction_extensions: &Exts, type_resolver: &Resolver, call_info: &ExtrinsicCallInfo, ext_info: &ExtrinsicExtensionInfo, -) -> Result, ExtrinsicEncodeError> +) -> Result, ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -332,13 +515,45 @@ where //// V5 +/// Encode a V5 bare extrinsic (also known as an inherent), ready to submit. +/// +/// V5 bare extrinsics contain only call data with no transaction extensions or signature. +/// They are functionally equivalent to V4 unsigned extrinsics and are typically used for +/// inherents (data provided by block authors). +/// +/// This is the same as [`encode_v5_bare_to`], but returns the encoded extrinsic as a `Vec`, +/// rather than accepting a mutable output buffer. +/// +/// # Example +/// +/// ```rust +/// use frame_decode::extrinsics::encode_v5_bare; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata_10000000_9180.scale").unwrap(); +/// let RuntimeMetadata::V14(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// // Encode a call to Timestamp.set with an argument. +/// let call_data = scale_value::value!({ +/// now: 1234567890u64, +/// }); +/// +/// let encoded = encode_v5_bare( +/// "Timestamp", +/// "set", +/// &call_data, +/// &metadata, +/// &metadata.types, +/// ).unwrap(); +/// ``` pub fn encode_v5_bare( pallet_name: &str, call_name: &str, call_data: &CallData, info: &Info, type_resolver: &Resolver, -) -> Result, ExtrinsicEncodeError> +) -> Result, ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -356,6 +571,36 @@ where Ok(out) } +/// Encode a V5 bare extrinsic (also known as an inherent) to a provided output buffer. +/// +/// This is the same as [`encode_v5_bare`], but writes the encoded extrinsic to the provided +/// `Vec` rather than returning a new one. +/// +/// # Example +/// +/// ```rust +/// use frame_decode::extrinsics::encode_v5_bare_to; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata_10000000_9180.scale").unwrap(); +/// let RuntimeMetadata::V14(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// // Encode a call to Timestamp.set with an argument. +/// let call_data = scale_value::value!({ +/// now: 1234567890u64, +/// }); +/// +/// let mut encoded = Vec::new(); +/// encode_v5_bare_to( +/// "Timestamp", +/// "set", +/// &call_data, +/// &metadata, +/// &metadata.types, +/// &mut encoded, +/// ).unwrap(); +/// ``` pub fn encode_v5_bare_to( pallet_name: &str, call_name: &str, @@ -363,7 +608,7 @@ pub fn encode_v5_bare_to( info: &Info, type_resolver: &Resolver, out: &mut Vec, -) -> Result<(), ExtrinsicEncodeError> +) -> Result<(), ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -373,7 +618,7 @@ where .extrinsic_call_info_by_name(pallet_name, call_name) .map_err(|i| i.into_owned()) .map_err(ExtrinsicEncodeError::CannotGetInfo)?; - + encode_v5_bare_with_info_to( call_data, type_resolver, @@ -382,6 +627,12 @@ where ) } +/// Encode a V5 bare extrinsic (also known as an inherent) to a provided output buffer, +/// using pre-computed call information. +/// +/// Unlike [`encode_v5_bare_to`], which obtains the call info internally given the pallet and call names, +/// this function takes the call info as an argument. This is useful if you already have the call info available, +/// for example if you are encoding multiple extrinsics for the same call. pub fn encode_v5_bare_with_info_to( call_data: &CallData, type_resolver: &Resolver, @@ -391,7 +642,7 @@ pub fn encode_v5_bare_with_info_to( where CallData: EncodeAsFields, Resolver: TypeResolver, -{ +{ encode_unsigned_at_version_with_info_to( call_data, &call_info, @@ -401,6 +652,49 @@ where ) } +/// Determine the best transaction extension version to use for a V5 general extrinsic. +/// +/// V5 general extrinsics support multiple versions of transaction extensions. This function +/// iterates through the available extension versions and returns the first version for which +/// all required extension data is provided. +/// +/// Use this function to determine which `transaction_extension_version` to pass to +/// [`encode_v5_general`] or [`encode_v5_general_to`]. +/// +/// # Errors +/// +/// Returns [`ExtrinsicEncodeError::CannotFindGoodExtensionVersion`] if no extension version +/// can be found for which all required data is available. +/// +/// # Example +/// +/// ```rust,ignore +/// use frame_decode::extrinsics::{best_v5_general_transaction_extension_version, encode_v5_general}; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata.scale").unwrap(); +/// let RuntimeMetadata::V16(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// let transaction_extensions = /* your TransactionExtensions impl */; +/// +/// // Find the best extension version for your provided extensions +/// let ext_version = best_v5_general_transaction_extension_version( +/// &transaction_extensions, +/// &metadata, +/// ).unwrap(); +/// +/// // Use this version when encoding the extrinsic +/// let encoded = encode_v5_general( +/// "Balances", +/// "transfer_keep_alive", +/// &call_data, +/// ext_version, +/// &transaction_extensions, +/// &metadata, +/// &metadata.types, +/// ).unwrap(); +/// ``` pub fn best_v5_general_transaction_extension_version( transaction_extensions: &Exts, info: &Info, @@ -437,6 +731,46 @@ where Err(ExtrinsicEncodeError::CannotFindGoodExtensionVersion) } +/// Encode a V5 general extrinsic, ready to submit. +/// +/// V5 general extrinsics include transaction extensions but no separate signature field. +/// Instead, the signature (if needed) is provided as part of one of the transaction extensions. +/// This is the new extrinsic format introduced in newer Substrate runtimes. +/// +/// Use [`best_v5_general_transaction_extension_version`] to determine which extension version +/// to use based on the extensions you have available. +/// +/// This is the same as [`encode_v5_general_to`], but returns the encoded extrinsic as a `Vec`, +/// rather than accepting a mutable output buffer. +/// +/// # Example +/// +/// ```rust,ignore +/// use frame_decode::extrinsics::{encode_v5_general, best_v5_general_transaction_extension_version}; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata.scale").unwrap(); +/// let RuntimeMetadata::V16(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// let call_data = /* ... */; +/// let transaction_extensions = /* your TransactionExtensions impl */; +/// +/// let ext_version = best_v5_general_transaction_extension_version( +/// &transaction_extensions, +/// &metadata, +/// ).unwrap(); +/// +/// let encoded = encode_v5_general( +/// "Balances", +/// "transfer_keep_alive", +/// &call_data, +/// ext_version, +/// &transaction_extensions, +/// &metadata, +/// &metadata.types, +/// ).unwrap(); +/// ``` pub fn encode_v5_general( pallet_name: &str, call_name: &str, @@ -466,6 +800,44 @@ where Ok(out) } +/// Encode a V5 general extrinsic to a provided output buffer. +/// +/// V5 general extrinsics include transaction extensions but no separate signature field. +/// Instead, the signature (if needed) is provided as part of one of the transaction extensions. +/// +/// This is the same as [`encode_v5_general`], but writes the encoded extrinsic to the provided +/// `Vec` rather than returning a new one. +/// +/// # Example +/// +/// ```rust,ignore +/// use frame_decode::extrinsics::{encode_v5_general_to, best_v5_general_transaction_extension_version}; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata.scale").unwrap(); +/// let RuntimeMetadata::V16(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// let call_data = /* ... */; +/// let transaction_extensions = /* your TransactionExtensions impl */; +/// +/// let ext_version = best_v5_general_transaction_extension_version( +/// &transaction_extensions, +/// &metadata, +/// ).unwrap(); +/// +/// let mut encoded = Vec::new(); +/// encode_v5_general_to( +/// "Balances", +/// "transfer_keep_alive", +/// &call_data, +/// ext_version, +/// &transaction_extensions, +/// &metadata, +/// &metadata.types, +/// &mut encoded, +/// ).unwrap(); +/// ``` pub fn encode_v5_general_to( pallet_name: &str, call_name: &str, @@ -503,6 +875,11 @@ where ) } +/// Encode a V5 general extrinsic to a provided output buffer, using pre-computed type information. +/// +/// Unlike [`encode_v5_general_to`], which obtains the call and extension info internally +/// given the pallet and call names, this function takes these as arguments. This is useful if you +/// already have this information available, for example if you are encoding multiple extrinsics. pub fn encode_v5_general_with_info_to( call_data: &CallData, transaction_extension_version: u8, @@ -542,6 +919,51 @@ where Ok(()) } +/// Encode the signer payload for a V5 general extrinsic. +/// +/// The signer payload is the data that should be signed to produce the signature for +/// a general extrinsic. It consists of the encoded call data, the transaction extension +/// values (for the signer payload), and the transaction extension implicit data. +/// +/// Unlike [`encode_v4_signer_payload`], which conditionally hashes the payload if it exceeds +/// 256 bytes, V5 signer payloads are always hashed using Blake2-256, returning a fixed 32-byte +/// array. +/// +/// Use this function to obtain the bytes that should be signed, then include the resulting +/// signature in the appropriate transaction extension when calling [`encode_v5_general`]. +/// +/// # Example +/// +/// ```rust,ignore +/// use frame_decode::extrinsics::{encode_v5_signer_payload, best_v5_general_transaction_extension_version}; +/// use frame_metadata::RuntimeMetadata; +/// use parity_scale_codec::Decode; +/// +/// let metadata_bytes = std::fs::read("artifacts/metadata.scale").unwrap(); +/// let RuntimeMetadata::V16(metadata) = RuntimeMetadata::decode(&mut &*metadata_bytes).unwrap() else { return }; +/// +/// let call_data = /* ... */; +/// let transaction_extensions = /* your TransactionExtensions impl */; +/// +/// let ext_version = best_v5_general_transaction_extension_version( +/// &transaction_extensions, +/// &metadata, +/// ).unwrap(); +/// +/// // Get the 32-byte payload hash to sign +/// let signer_payload = encode_v5_signer_payload( +/// "Balances", +/// "transfer_keep_alive", +/// &call_data, +/// ext_version, +/// &transaction_extensions, +/// &metadata, +/// &metadata.types, +/// ).unwrap(); +/// +/// // Sign the payload with your signing key, then include the signature +/// // in your transaction extensions when calling encode_v5_general. +/// ``` pub fn encode_v5_signer_payload( pallet_name: &str, call_name: &str, @@ -550,7 +972,7 @@ pub fn encode_v5_signer_payload( transaction_extensions: &Exts, info: &Info, type_resolver: &Resolver, -) -> Result<[u8; 32], ExtrinsicEncodeError> +) -> Result<[u8; 32], ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -577,13 +999,22 @@ where ) } +/// Encode the signer payload for a V5 general extrinsic, using pre-computed type information. +/// +/// Unlike [`encode_v5_signer_payload`], which obtains the call and extension info internally +/// given the pallet and call names, this function takes these as arguments. This is useful if you +/// already have this information available. +/// +/// The signer payload consists of the encoded call data, the transaction extension values +/// (for the signer payload), and the transaction extension implicit data. The result is always +/// hashed using Blake2-256, returning a fixed 32-byte array. pub fn encode_v5_signer_payload_with_info( call_data: &CallData, transaction_extensions: &Exts, type_resolver: &Resolver, call_info: &ExtrinsicCallInfo, ext_info: &ExtrinsicExtensionInfo, -) -> Result<[u8; 32], ExtrinsicEncodeError> +) -> Result<[u8; 32], ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, diff --git a/src/methods/extrinsic_encoder/transaction_extensions.rs b/src/methods/extrinsic_encoder/transaction_extensions.rs index 96ca38a..af3ab44 100644 --- a/src/methods/extrinsic_encoder/transaction_extensions.rs +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -71,11 +71,15 @@ pub trait TransactionExtensions { /// This error will be returned if any of the methods in [`TransactionExtensions`] fail. #[derive(Debug, thiserror::Error)] pub enum TransactionExtensionsError { + /// The requested transaction extension could not be found. #[error("Cannot encode transaction extension '{0}': This extension could not be found")] NotFound(String), + /// An error occurred while encoding the transaction extension. #[error("Cannot encode transaction extension '{extension_name}': {error}")] Other { + /// The name of the extension that failed to encode. extension_name: String, + /// The underlying error. error: TransactionExtensionError, } } diff --git a/src/methods/extrinsic_type_info.rs b/src/methods/extrinsic_type_info.rs index 323c0e9..7367294 100644 --- a/src/methods/extrinsic_type_info.rs +++ b/src/methods/extrinsic_type_info.rs @@ -77,11 +77,19 @@ pub enum ExtrinsicInfoError<'a> { PalletNotFound { index: u8, }, + PalletNotFoundByName { + name: Cow<'a, str>, + }, CallNotFound { index: u8, pallet_index: u8, pallet_name: Cow<'a, str>, }, + CallNotFoundByName { + call_name: Cow<'a, str>, + pallet_index: u8, + pallet_name: Cow<'a, str>, + }, #[cfg(feature = "legacy")] CannotParseTypeName { name: Cow<'a, str>, @@ -118,6 +126,9 @@ impl core::fmt::Display for ExtrinsicInfoError<'_> { ExtrinsicInfoError::PalletNotFound { index } => { write!(f, "Pallet with index {index} not found") } + ExtrinsicInfoError::PalletNotFoundByName { name } => { + write!(f, "Pallet with name '{name}' not found") + } ExtrinsicInfoError::CallNotFound { index, pallet_index, @@ -128,6 +139,16 @@ impl core::fmt::Display for ExtrinsicInfoError<'_> { "Call with index {index} not found in pallet '{pallet_name}' (pallet index {pallet_index})." ) } + ExtrinsicInfoError::CallNotFoundByName { + call_name, + pallet_index, + pallet_name, + } => { + write!( + f, + "Call with name '{call_name}' not found in pallet '{pallet_name}' (pallet index {pallet_index})." + ) + } #[cfg(feature = "legacy")] ExtrinsicInfoError::CannotParseTypeName { name, reason } => { write!(f, "Cannot parse type name '{name}':\n\n{reason}") @@ -187,6 +208,11 @@ impl ExtrinsicInfoError<'_> { ExtrinsicInfoError::PalletNotFound { index } => { ExtrinsicInfoError::PalletNotFound { index } } + ExtrinsicInfoError::PalletNotFoundByName { name } => { + ExtrinsicInfoError::PalletNotFoundByName { + name: Cow::Owned(name.into_owned()), + } + } ExtrinsicInfoError::CallNotFound { index, pallet_index, @@ -196,6 +222,15 @@ impl ExtrinsicInfoError<'_> { pallet_index, pallet_name: Cow::Owned(pallet_name.into_owned()), }, + ExtrinsicInfoError::CallNotFoundByName { + call_name, + pallet_index, + pallet_name, + } => ExtrinsicInfoError::CallNotFoundByName { + call_name: Cow::Owned(call_name.into_owned()), + pallet_index, + pallet_name: Cow::Owned(pallet_name.into_owned()), + }, #[cfg(feature = "legacy")] ExtrinsicInfoError::CannotParseTypeName { name, reason } => { ExtrinsicInfoError::CannotParseTypeName { @@ -280,7 +315,87 @@ pub struct ExtrinsicExtensionInfo<'info, TypeId> { pub extension_ids: Vec>, } -macro_rules! impl_call_arg_ids_body_for_v14_to_v16 { +macro_rules! impl_call_info_by_name_body_for_v14_to_v16 { + ($self:ident, $pallet_name:ident, $call_name:ident) => {{ + use alloc::string::ToString; + + let pallet = $self + .pallets + .iter() + .find(|p| p.name == $pallet_name) + .ok_or_else(|| ExtrinsicInfoError::PalletNotFoundByName { + name: Cow::Owned($pallet_name.to_string()), + })?; + + let pallet_index = pallet.index; + + let calls_id = pallet + .calls + .as_ref() + .ok_or_else(|| ExtrinsicInfoError::CallNotFoundByName { + call_name: Cow::Owned($call_name.to_string()), + pallet_index, + pallet_name: Cow::Borrowed(&pallet.name), + })? + .ty + .id; + + let calls_ty = + $self + .types + .resolve(calls_id) + .ok_or_else(|| ExtrinsicInfoError::CallsTypeNotFound { + id: calls_id, + pallet_index, + pallet_name: Cow::Borrowed(&pallet.name), + })?; + + let calls_enum = match &calls_ty.type_def { + scale_info::TypeDef::Variant(v) => v, + _ => { + return Err(ExtrinsicInfoError::CallsTypeShouldBeVariant { + id: calls_id, + pallet_index, + pallet_name: Cow::Borrowed(&pallet.name), + }); + } + }; + + let call_variant = calls_enum + .variants + .iter() + .find(|v| v.name == $call_name) + .ok_or_else(|| ExtrinsicInfoError::CallNotFoundByName { + call_name: Cow::Owned($call_name.to_string()), + pallet_index, + pallet_name: Cow::Borrowed(&pallet.name), + })?; + + let args = call_variant + .fields + .iter() + .map(|f| { + let id = f.ty.id; + let name = f + .name + .as_ref() + .map(|n| Cow::Borrowed(&**n)) + .unwrap_or(Cow::Owned(String::new())); + ExtrinsicInfoArg { id, name } + }) + .collect(); + + Ok(ExtrinsicCallInfo { + pallet_index, + call_index: call_variant.index, + pallet_name: Cow::Borrowed(&pallet.name), + call_name: Cow::Borrowed(&call_variant.name), + args, + }) + }}; +} + +macro_rules! impl_call_info_by_index_body_for_v14_to_v16 { ($self:ident, $pallet_index:ident, $call_index:ident) => {{ let pallet = $self .pallets @@ -348,7 +463,9 @@ macro_rules! impl_call_arg_ids_body_for_v14_to_v16 { }) .collect(); - Ok(ExtrinsicCallInfoByIndex { + Ok(ExtrinsicCallInfo { + pallet_index: $pallet_index, + call_index: $call_index, pallet_name: Cow::Borrowed(pallet_name), call_name: Cow::Borrowed(&call_variant.name), args, @@ -362,8 +479,15 @@ impl ExtrinsicTypeInfo for frame_metadata::v14::RuntimeMetadataV14 { &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { - impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) + ) -> Result, ExtrinsicInfoError<'_>> { + impl_call_info_by_index_body_for_v14_to_v16!(self, pallet_index, call_index) + } + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>> { + impl_call_info_by_name_body_for_v14_to_v16!(self, pallet_name, call_name) } fn extrinsic_signature_info( &self, @@ -393,6 +517,11 @@ impl ExtrinsicTypeInfo for frame_metadata::v14::RuntimeMetadataV14 { Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } impl ExtrinsicTypeInfo for frame_metadata::v15::RuntimeMetadataV15 { @@ -401,8 +530,15 @@ impl ExtrinsicTypeInfo for frame_metadata::v15::RuntimeMetadataV15 { &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { - impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) + ) -> Result, ExtrinsicInfoError<'_>> { + impl_call_info_by_index_body_for_v14_to_v16!(self, pallet_index, call_index) + } + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>> { + impl_call_info_by_name_body_for_v14_to_v16!(self, pallet_name, call_name) } fn extrinsic_signature_info( &self, @@ -430,6 +566,11 @@ impl ExtrinsicTypeInfo for frame_metadata::v15::RuntimeMetadataV15 { Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } impl ExtrinsicTypeInfo for frame_metadata::v16::RuntimeMetadataV16 { @@ -438,8 +579,15 @@ impl ExtrinsicTypeInfo for frame_metadata::v16::RuntimeMetadataV16 { &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { - impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) + ) -> Result, ExtrinsicInfoError<'_>> { + impl_call_info_by_index_body_for_v14_to_v16!(self, pallet_index, call_index) + } + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>> { + impl_call_info_by_name_body_for_v14_to_v16!(self, pallet_name, call_name) } fn extrinsic_signature_info( &self, @@ -485,6 +633,11 @@ impl ExtrinsicTypeInfo for frame_metadata::v16::RuntimeMetadataV16 { Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(self.extrinsic.transaction_extensions_by_version.keys().copied()) + } } fn get_v14_extrinsic_parts( @@ -557,7 +710,10 @@ fn err_if_bad_extension_version<'a>( #[cfg(feature = "legacy")] const _: () = { - macro_rules! impl_extrinsic_info_body_for_v8_to_v11 { + use crate::utils::as_decoded; + use scale_info_legacy::LookupName; + + macro_rules! impl_extrinsic_info_by_index_body_for_v8_to_v11 { ($self:ident, $pallet_index:ident, $call_index:ident) => {{ let modules = as_decoded(&$self.modules); @@ -609,7 +765,82 @@ const _: () = { }) .collect::>()?; - Ok(ExtrinsicCallInfoByIndex { + Ok(ExtrinsicCallInfo { + pallet_index: $pallet_index, + call_index: $call_index, + pallet_name: Cow::Borrowed(m_name), + call_name: Cow::Borrowed(c_name), + args, + }) + }}; + } + + macro_rules! impl_extrinsic_info_by_name_body_for_v8_to_v11 { + ($self:ident, $pallet_name_arg:ident, $call_name_arg:ident) => {{ + use alloc::string::ToString; + + let modules = as_decoded(&$self.modules); + + let (pallet_index, m) = modules + .iter() + .filter(|m| m.calls.is_some()) + .enumerate() + .find(|(_, m)| { + let name: &str = as_decoded(&m.name).as_ref(); + name == $pallet_name_arg + }) + .ok_or_else(|| ExtrinsicInfoError::PalletNotFoundByName { + name: Cow::Owned($pallet_name_arg.to_string()), + })?; + + let pallet_index = pallet_index as u8; + + // as_ref to work when scale-info returns `&static str` + // instead of `String` in no-std mode. + let m_name: &str = as_decoded(&m.name).as_ref(); + + let calls = m + .calls + .as_ref() + .ok_or_else(|| ExtrinsicInfoError::CallNotFoundByName { + call_name: Cow::Owned($call_name_arg.to_string()), + pallet_index, + pallet_name: Cow::Borrowed(m_name), + })?; + + let calls = as_decoded(calls); + + let (call_index, call) = calls.iter().enumerate().find(|(_, c)| { + let name: &str = as_decoded(&c.name).as_ref(); + name == $call_name_arg + }).ok_or_else(|| { + ExtrinsicInfoError::CallNotFoundByName { + call_name: Cow::Owned($call_name_arg.to_string()), + pallet_index, + pallet_name: Cow::Borrowed(m_name), + } + })?; + + let c_name: &str = as_decoded(&call.name).as_ref(); + + let args = as_decoded(&call.arguments); + + let args = args + .iter() + .map(|a| { + let ty: &str = as_decoded(&a.ty).as_ref(); + let id = parse_lookup_name(ty)?.in_pallet(m_name); + let name: &str = as_decoded(&a.name).as_ref(); + Ok(ExtrinsicInfoArg { + id, + name: Cow::Borrowed(name), + }) + }) + .collect::>()?; + + Ok(ExtrinsicCallInfo { + pallet_index, + call_index: call_index as u8, pallet_name: Cow::Borrowed(m_name), call_name: Cow::Borrowed(c_name), args, @@ -625,8 +856,15 @@ const _: () = { &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { - impl_extrinsic_info_body_for_v8_to_v11!(self, pallet_index, call_index) + ) -> Result, ExtrinsicInfoError<'_>> { + impl_extrinsic_info_by_index_body_for_v8_to_v11!(self, pallet_index, call_index) + } + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>> { + impl_extrinsic_info_by_name_body_for_v8_to_v11!(self, pallet_name, call_name) } fn extrinsic_signature_info( &self, @@ -649,6 +887,11 @@ const _: () = { }]), }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } }; } @@ -663,8 +906,15 @@ const _: () = { &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { - impl_extrinsic_info_body_for_v8_to_v11!(self, pallet_index, call_index) + ) -> Result, ExtrinsicInfoError<'_>> { + impl_extrinsic_info_by_index_body_for_v8_to_v11!(self, pallet_index, call_index) + } + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>> { + impl_extrinsic_info_by_name_body_for_v8_to_v11!(self, pallet_name, call_name) } fn extrinsic_signature_info( &self, @@ -699,6 +949,11 @@ const _: () = { Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } macro_rules! impl_for_v12_to_v13 { @@ -709,7 +964,7 @@ const _: () = { &self, pallet_index: u8, call_index: u8, - ) -> Result, ExtrinsicInfoError<'_>> { + ) -> Result, ExtrinsicInfoError<'_>> { let modules = as_decoded(&self.modules); let m = modules @@ -759,7 +1014,81 @@ const _: () = { }) .collect::>()?; - Ok(ExtrinsicCallInfoByIndex { + Ok(ExtrinsicCallInfo { + pallet_index, + call_index, + pallet_name: Cow::Borrowed(m_name), + call_name: Cow::Borrowed(c_name), + args, + }) + } + fn extrinsic_call_info_by_name( + &self, + pallet_name: &str, + call_name: &str, + ) -> Result, ExtrinsicInfoError<'_>> { + use alloc::string::ToString; + + let modules = as_decoded(&self.modules); + + let m = modules + .iter() + .find(|m| { + let name: &str = as_decoded(&m.name).as_ref(); + name == pallet_name + }) + .ok_or_else(|| ExtrinsicInfoError::PalletNotFoundByName { + name: Cow::Owned(pallet_name.to_string()), + })?; + + let pallet_index = m.index; + + // as_ref to work when scale-info returns `&static str` + // instead of `String` in no-std mode. + let m_name: &str = as_decoded(&m.name).as_ref(); + + let calls = + m.calls + .as_ref() + .ok_or_else(|| ExtrinsicInfoError::CallNotFoundByName { + call_name: Cow::Owned(call_name.to_string()), + pallet_index, + pallet_name: Cow::Borrowed(m_name), + })?; + + let calls = as_decoded(calls); + + let (call_index, call) = calls.iter().enumerate().find(|(_, c)| { + let name: &str = as_decoded(&c.name).as_ref(); + name == call_name + }).ok_or_else(|| { + ExtrinsicInfoError::CallNotFoundByName { + call_name: Cow::Owned(call_name.to_string()), + pallet_index, + pallet_name: Cow::Borrowed(m_name), + } + })?; + + let c_name: &str = as_decoded(&call.name).as_ref(); + + let args = as_decoded(&call.arguments); + + let args = args + .iter() + .map(|a| { + let ty: &str = as_decoded(&a.ty).as_ref(); + let id = parse_lookup_name(ty)?.in_pallet(m_name); + let name: &str = as_decoded(&a.name).as_ref(); + Ok(ExtrinsicInfoArg { + id, + name: Cow::Borrowed(name), + }) + }) + .collect::>()?; + + Ok(ExtrinsicCallInfo { + pallet_index, + call_index: call_index as u8, pallet_name: Cow::Borrowed(m_name), call_name: Cow::Borrowed(c_name), args, @@ -798,6 +1127,11 @@ const _: () = { Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } }; } From f3edcca9d1a7ef87b0633d436148998309bb3901 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 10 Feb 2026 12:07:19 +0000 Subject: [PATCH 03/12] use alloc types where needed --- src/lib.rs | 20 +-- src/methods/extrinsic_encoder.rs | 154 ++++++++---------- .../transaction_extension.rs | 26 +-- .../transaction_extensions.rs | 126 +++++++------- src/methods/extrinsic_type_info.rs | 51 +++--- 5 files changed, 188 insertions(+), 189 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c5925ba..2a553ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,17 +40,15 @@ pub mod extrinsics { ExtrinsicType, NamedArg, decode_extrinsic, }; pub use crate::methods::extrinsic_encoder::{ - TransactionExtension, - TransactionExtensionError, - TransactionExtensions, - TransactionExtensionsError, - ExtrinsicEncodeError, - encode_v4_signed, encode_v4_signed_to, - encode_v4_signed_with_info_to, encode_v4_signer_payload, encode_v4_signer_payload_with_info, - encode_v4_unsigned, encode_v4_unsigned_to, encode_v4_unsigned_with_info_to, encode_v5_bare, - encode_v5_bare_to, encode_v5_bare_with_info_to, encode_v5_general, encode_v5_general_to, - encode_v5_general_with_info_to, encode_v5_signer_payload, encode_v5_signer_payload_with_info, - best_v5_general_transaction_extension_version, + ExtrinsicEncodeError, TransactionExtension, TransactionExtensionError, + TransactionExtensions, TransactionExtensionsError, + best_v5_general_transaction_extension_version, encode_v4_signed, encode_v4_signed_to, + encode_v4_signed_with_info_to, encode_v4_signer_payload, + encode_v4_signer_payload_with_info, encode_v4_unsigned, encode_v4_unsigned_to, + encode_v4_unsigned_with_info_to, encode_v5_bare, encode_v5_bare_to, + encode_v5_bare_with_info_to, encode_v5_general, encode_v5_general_to, + encode_v5_general_with_info_to, encode_v5_signer_payload, + encode_v5_signer_payload_with_info, }; pub use crate::methods::extrinsic_type_info::{ ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicInfoArg, ExtrinsicInfoError, diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index 748b1df..e00e158 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -13,22 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod transaction_extensions; mod transaction_extension; +mod transaction_extensions; -use super::extrinsic_type_info::{ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicSignatureInfo, ExtrinsicTypeInfo, ExtrinsicInfoError}; -use scale_encode::{EncodeAsType, EncodeAsFields}; -use scale_type_resolver::{TypeResolver, Field}; +use super::extrinsic_type_info::{ + ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicInfoError, ExtrinsicSignatureInfo, + ExtrinsicTypeInfo, +}; +use alloc::vec::Vec; use parity_scale_codec::Encode; +use scale_encode::{EncodeAsFields, EncodeAsType}; +use scale_type_resolver::{Field, TypeResolver}; -pub use transaction_extension::{ - TransactionExtension, - TransactionExtensionError, -}; -pub use transaction_extensions::{ - TransactionExtensions, - TransactionExtensionsError, -}; +pub use transaction_extension::{TransactionExtension, TransactionExtensionError}; +pub use transaction_extensions::{TransactionExtensions, TransactionExtensionsError}; /// An error returned trying to encode extrinsic call data. #[non_exhaustive] @@ -45,8 +43,10 @@ pub enum ExtrinsicEncodeError { CannotEncodeSignature(scale_encode::Error), #[error("Extrinsic encoding failed: cannot encode transaction extensions: {0}")] TransactionExtensions(TransactionExtensionsError), - #[error("Extrinsic encoding failed: cannot find a transaction extension version which enough data was given for.")] - CannotFindGoodExtensionVersion + #[error( + "Extrinsic encoding failed: cannot find a transaction extension version which enough data was given for." + )] + CannotFindGoodExtensionVersion, } //// V4 @@ -86,7 +86,7 @@ pub fn encode_v4_unsigned( call_data: &CallData, info: &Info, type_resolver: &Resolver, -) -> Result, ExtrinsicEncodeError> +) -> Result, ExtrinsicEncodeError> where CallData: EncodeAsFields, Resolver: TypeResolver, @@ -152,12 +152,7 @@ where .map_err(|i| i.into_owned()) .map_err(ExtrinsicEncodeError::CannotGetInfo)?; - encode_v4_unsigned_with_info_to( - call_data, - type_resolver, - &call_info, - out - ) + encode_v4_unsigned_with_info_to(call_data, type_resolver, &call_info, out) } /// Encode a V4 unsigned extrinsic (also known as an inherent) to a provided output buffer, @@ -181,7 +176,7 @@ where &call_info, type_resolver, TransactionVersion::V4, - out + out, ) } @@ -368,11 +363,19 @@ where (0b10000000 + 4u8).encode_to(&mut encoded_inner); // Who is this transaction from (corresponds to public key of signature) address - .encode_as_type_to(sig_info.address_id.clone(), type_resolver, &mut encoded_inner) + .encode_as_type_to( + sig_info.address_id.clone(), + type_resolver, + &mut encoded_inner, + ) .map_err(ExtrinsicEncodeError::CannotEncodeAddress)?; // Signature for the above identity signature - .encode_as_type_to(sig_info.signature_id.clone(), type_resolver, &mut encoded_inner) + .encode_as_type_to( + sig_info.signature_id.clone(), + type_resolver, + &mut encoded_inner, + ) .map_err(ExtrinsicEncodeError::CannotEncodeSignature)?; // Signed extensions (now Transaction Extensions) for ext in &ext_info.extension_ids { @@ -381,12 +384,8 @@ where .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // And now the actual call data, ie the arguments we're passing to the call - encode_call_data_with_info_to( - call_data, - call_info, - type_resolver, - &mut encoded_inner - ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner) + .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; // Now, encoding these inner bytes prefixes the compact length to the beginning: encoded_inner.encode_to(out); @@ -460,7 +459,7 @@ where type_resolver, &call_info, &ext_info, - ) + ) } /// Encode the signer payload for a V4 signed extrinsic, using pre-computed type information. @@ -487,16 +486,17 @@ where let mut out = Vec::new(); // First encode call data - encode_call_data_with_info_to( - call_data, - &call_info, - type_resolver, - &mut out - ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out) + .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) for ext in &ext_info.extension_ids { transaction_extensions - .encode_extension_value_for_signer_payload_to(&ext.name, ext.id.clone(), type_resolver, &mut out) + .encode_extension_value_for_signer_payload_to( + &ext.name, + ext.id.clone(), + type_resolver, + &mut out, + ) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) @@ -619,12 +619,7 @@ where .map_err(|i| i.into_owned()) .map_err(ExtrinsicEncodeError::CannotGetInfo)?; - encode_v5_bare_with_info_to( - call_data, - type_resolver, - &call_info, - out - ) + encode_v5_bare_with_info_to(call_data, type_resolver, &call_info, out) } /// Encode a V5 bare extrinsic (also known as an inherent) to a provided output buffer, @@ -648,7 +643,7 @@ where &call_info, type_resolver, TransactionVersion::V5, - out + out, ) } @@ -717,14 +712,15 @@ where .map_err(ExtrinsicEncodeError::CannotGetInfo)?; // Do we have all of the extension data for this version? - let have_data = ext_info.extension_ids.iter().all(|e| { - transaction_extensions.contains_extension(&e.name) - }); + let have_data = ext_info + .extension_ids + .iter() + .all(|e| transaction_extensions.contains_extension(&e.name)); // If we have all of the data we need, encode the extrinsic, // else loop and try the next extension version. if have_data { - return Ok(ext_version) + return Ok(ext_version); } } @@ -872,7 +868,7 @@ where &call_info, &ext_info, out, - ) + ); } /// Encode a V5 general extrinsic to a provided output buffer, using pre-computed type information. @@ -907,12 +903,8 @@ where .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // And now the actual call data, ie the arguments we're passing to the call - encode_call_data_with_info_to( - call_data, - call_info, - type_resolver, - &mut encoded_inner - ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner) + .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; // Now, encoding these inner bytes prefixes the compact length to the beginning: encoded_inner.encode_to(out); @@ -996,7 +988,7 @@ where type_resolver, &call_info, &ext_info, - ) + ) } /// Encode the signer payload for a V5 general extrinsic, using pre-computed type information. @@ -1023,17 +1015,18 @@ where let mut out = Vec::new(); // First encode call data - encode_call_data_with_info_to( - call_data, - &call_info, - type_resolver, - &mut out - ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out) + .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) for ext in &ext_info.extension_ids { transaction_extensions - .encode_extension_value_for_signer_payload_to(&ext.name, ext.id.clone(), type_resolver, &mut out) + .encode_extension_value_for_signer_payload_to( + &ext.name, + ext.id.clone(), + type_resolver, + &mut out, + ) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } @@ -1048,7 +1041,7 @@ where Ok(sp_crypto_hashing::blake2_256(&out)) } -// V4 unsigned and V5 bare extrinsics are basically encoded +// V4 unsigned and V5 bare extrinsics are basically encoded // in the same way; this helper can do either. fn encode_unsigned_at_version_with_info_to( call_data: &CallData, @@ -1056,7 +1049,7 @@ fn encode_unsigned_at_version_with_info_to( type_resolver: &Resolver, tx_version: TransactionVersion, out: &mut Vec, -) -> Result<(), ExtrinsicEncodeError> +) -> Result<(), ExtrinsicEncodeError> where Resolver: TypeResolver, CallData: EncodeAsFields, @@ -1070,15 +1063,11 @@ where call_info.pallet_index.encode_to(&mut out); call_info.call_index.encode_to(&mut out); // Then the arguments for the call: - encode_call_data_with_info_to( - call_data, - call_info, - type_resolver, - &mut out - ).map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out) + .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; out }; - + // Encode the inner vec to prefix the compact length to it: inner.encode_to(out); Ok(()) @@ -1093,24 +1082,21 @@ enum TransactionVersion { // Encoding call data is the same for any extrinsic; this method does this part. fn encode_call_data_with_info_to( - call_data: &CallData, - call_info: &ExtrinsicCallInfo, + call_data: &CallData, + call_info: &ExtrinsicCallInfo, type_resolver: &Resolver, out: &mut Vec, -) -> Result<(), scale_encode::Error> +) -> Result<(), scale_encode::Error> where Resolver: TypeResolver, CallData: EncodeAsFields, { - let mut fields = call_info.args.iter().map(|arg| { - Field { - name: Some(&*arg.name), - id: arg.id.clone() - } + let mut fields = call_info.args.iter().map(|arg| Field { + name: Some(&*arg.name), + id: arg.id.clone(), }); - call_data - .encode_as_fields_to(&mut fields, type_resolver, out)?; + call_data.encode_as_fields_to(&mut fields, type_resolver, out)?; Ok(()) -} \ No newline at end of file +} diff --git a/src/methods/extrinsic_encoder/transaction_extension.rs b/src/methods/extrinsic_encoder/transaction_extension.rs index 51bb1aa..1fca06a 100644 --- a/src/methods/extrinsic_encoder/transaction_extension.rs +++ b/src/methods/extrinsic_encoder/transaction_extension.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use alloc::boxed::Box; +use alloc::vec::Vec; use scale_type_resolver::TypeResolver; /// This can be implemented for anything which is a valid Substrate transaction extension. @@ -26,16 +28,16 @@ pub trait TransactionExtension { /// this should encode the value (ie the bytes that will appear in the /// transaction) to the provided `Vec`, or encode nothing and emit an error. fn encode_value_to( - &self, - type_id: Resolver::TypeId, - type_resolver: &Resolver, - out: &mut Vec + &self, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionError>; /// Given type information for the expected transaction extension, /// this should encode the value that will be signed as a part of the /// signer payload. - /// + /// /// This defaults to calling [`Self::encode_value_to`] if not implemented. /// In most cases this is fine, but for V5 extrinsics we can optionally provide /// the signature inside a transaction extension, and so that transaction would be @@ -43,9 +45,9 @@ pub trait TransactionExtension { /// method to encode nothing. fn encode_value_for_signer_payload_to( &self, - type_id: Resolver::TypeId, - type_resolver: &Resolver, - out: &mut Vec + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionError> { self.encode_value_to(type_id, type_resolver, out) } @@ -54,10 +56,10 @@ pub trait TransactionExtension { /// this should encode the implicit (ie the bytes that will appear in the /// signer payload) to the provided `Vec`, or encode nothing and emit an error. fn encode_implicit_to( - &self, - type_id: Resolver::TypeId, - type_resolver: &Resolver, - out: &mut Vec + &self, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionError>; } diff --git a/src/methods/extrinsic_encoder/transaction_extensions.rs b/src/methods/extrinsic_encoder/transaction_extensions.rs index af3ab44..ed25951 100644 --- a/src/methods/extrinsic_encoder/transaction_extensions.rs +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -13,15 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::transaction_extension::{TransactionExtension, TransactionExtensionError}; +use alloc::borrow::ToOwned; +use alloc::string::String; +use alloc::vec::Vec; use scale_type_resolver::TypeResolver; -use super::transaction_extension::{ TransactionExtension, TransactionExtensionError }; /// This trait can be implemented for anything which represents a set of transaction extensions. /// It's implemented by default for tuples of items which implement [`TransactionExtension`], /// and for slices of `&dyn TransactionExtension`. pub trait TransactionExtensions { /// Is a given transaction extension contained within this set? - fn contains_extension(&self, name: &str) -> bool; + fn contains_extension(&self, name: &str) -> bool; /// This will be called given the name of each transaction extension we /// wish to obtain the encoded bytes to. Implementations are expected to @@ -29,42 +32,42 @@ pub trait TransactionExtensions { /// or return an error if no such bytes can be written. fn encode_extension_value_to( &self, - name: &str, - type_id: Resolver::TypeId, - type_resolver: &Resolver, - out: &mut Vec + name: &str, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionsError>; /// This will be called given the name of each transaction extension we /// wish to obtain the encoded bytes to. Implementations are expected to - /// write the bytes that should be included in the **signer payload value - /// section** to the given [`Vec`], or return an error if no such bytes can be + /// write the bytes that should be included in the **signer payload value + /// section** to the given [`Vec`], or return an error if no such bytes can be /// written. - /// + /// /// This defaults to calling [`Self::encode_extension_value_to`] if not implemented. /// In most cases this is fine, but for V5 extrinsics we can optionally provide /// the signature inside a transaction extension, and so that transaction would be /// unable to encode anything for the signer payload. fn encode_extension_value_for_signer_payload_to( &self, - name: &str, - type_id: Resolver::TypeId, - type_resolver: &Resolver, - out: &mut Vec + name: &str, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionsError> { self.encode_extension_value_to(name, type_id, type_resolver, out) } /// This will be called given the name of each transaction extension we /// wish to obtain the encoded bytes to. Implementations are expected to - /// write the bytes that should be included in the **signer payload implicit** + /// write the bytes that should be included in the **signer payload implicit** /// to the given [`Vec`], or return an error if no such bytes can be written. fn encode_extension_implicit_to( &self, - name: &str, - type_id: Resolver::TypeId, - type_resolver: &Resolver, - out: &mut Vec + name: &str, + type_id: Resolver::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionsError>; } @@ -81,27 +84,30 @@ pub enum TransactionExtensionsError { extension_name: String, /// The underlying error. error: TransactionExtensionError, - } + }, } // `TransactionExtension` is object safe and so `TransactionExtensions` can be implemented // for slices of `&dyn TransactionExtension`s. -impl TransactionExtensions for [&dyn TransactionExtension] { +impl TransactionExtensions + for [&dyn TransactionExtension] +{ fn contains_extension(&self, name: &str) -> bool { self.iter().find(|e| e.extension_name() == name).is_some() } fn encode_extension_value_to( &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, - out: &mut Vec + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionsError> { let len = out.len(); for ext in self { if ext.extension_name() == name { - return ext.encode_value_to(type_id, type_resolver, out) + return ext + .encode_value_to(type_id, type_resolver, out) .map_err(|e| { // Protection: if we are returning an error then // no bytes should have been encoded to the given @@ -121,15 +127,16 @@ impl TransactionExtensions for [&dyn Transact fn encode_extension_value_for_signer_payload_to( &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, - out: &mut Vec + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionsError> { let len = out.len(); for ext in self { if ext.extension_name() == name { - return ext.encode_value_for_signer_payload_to(type_id, type_resolver, out) + return ext + .encode_value_for_signer_payload_to(type_id, type_resolver, out) .map_err(|e| { // Protection: if we are returning an error then // no bytes should have been encoded to the given @@ -146,18 +153,19 @@ impl TransactionExtensions for [&dyn Transact } Err(TransactionExtensionsError::NotFound(name.to_owned())) } - + fn encode_extension_implicit_to( &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, - out: &mut Vec + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, + out: &mut Vec, ) -> Result<(), TransactionExtensionsError> { let len = out.len(); for ext in self { if ext.extension_name() == name { - return ext.encode_implicit_to(type_id, type_resolver, out) + return ext + .encode_implicit_to(type_id, type_resolver, out) .map_err(|e| { // Protection: if we are returning an error then // no bytes should have been encoded to the given @@ -177,27 +185,27 @@ impl TransactionExtensions for [&dyn Transact } // Empty tuples impl `TransactionExtensions`: if called they emit a not found error. -impl TransactionExtensions for () { +impl TransactionExtensions for () { fn contains_extension(&self, _name: &str) -> bool { false } fn encode_extension_value_to( &self, - name: &str, - _type_id: ::TypeId, - _type_resolver: &Resolver, - _out: &mut Vec + name: &str, + _type_id: ::TypeId, + _type_resolver: &Resolver, + _out: &mut Vec, ) -> Result<(), TransactionExtensionsError> { Err(TransactionExtensionsError::NotFound(name.to_owned())) } fn encode_extension_implicit_to( &self, - name: &str, - _type_id: ::TypeId, - _type_resolver: &Resolver, - _out: &mut Vec + name: &str, + _type_id: ::TypeId, + _type_resolver: &Resolver, + _out: &mut Vec, ) -> Result<(), TransactionExtensionsError> { Err(TransactionExtensionsError::NotFound(name.to_owned())) } @@ -207,7 +215,7 @@ impl TransactionExtensions for () { // search through the tuple items to find it and call the appropriate encode method. macro_rules! impl_tuples { ($($ident:ident $index:tt),*) => { - impl TransactionExtensions for ($($ident,)*) + impl TransactionExtensions for ($($ident,)*) where $($ident: TransactionExtension,)* { @@ -217,14 +225,14 @@ macro_rules! impl_tuples { return true } )* - false + false } fn encode_extension_value_to( - &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, out: &mut Vec ) -> Result<(), TransactionExtensionsError> { let len = out.len(); @@ -251,10 +259,10 @@ macro_rules! impl_tuples { } fn encode_extension_value_for_signer_payload_to( - &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, out: &mut Vec ) -> Result<(), TransactionExtensionsError> { let len = out.len(); @@ -281,10 +289,10 @@ macro_rules! impl_tuples { } fn encode_extension_implicit_to( - &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, + &self, + name: &str, + type_id: ::TypeId, + type_resolver: &Resolver, out: &mut Vec ) -> Result<(), TransactionExtensionsError> { let len = out.len(); diff --git a/src/methods/extrinsic_type_info.rs b/src/methods/extrinsic_type_info.rs index 7367294..0cdd8b0 100644 --- a/src/methods/extrinsic_type_info.rs +++ b/src/methods/extrinsic_type_info.rs @@ -14,13 +14,10 @@ // limitations under the License. use alloc::borrow::Cow; -//use alloc::string::String; +use alloc::string::String; use alloc::vec::Vec; use core::write; -// #[cfg(feature = "legacy")] -// use {crate::utils::as_decoded, scale_info_legacy::LookupName}; - /// Implementations of this are responsible for handing back the information we need to /// encode and decode extrinsics. This is expected to be implemented for runtime metadata /// types or derivatives thereof where needed. @@ -54,19 +51,19 @@ pub trait ExtrinsicTypeInfo { ) -> Result, ExtrinsicInfoError<'_>>; /// Get the available transaction extension versions. Prior to runtimes supporting - /// V5 extrinsics this should not return any entries. In runtimes supporting V5 extrinsics, + /// V5 extrinsics this should not return any entries. In runtimes supporting V5 extrinsics, /// 1 or more versions should be returned. - /// + /// /// Versions should be returned in order from highest to lowest preference of /// whether they should be used for encoding extrinsics. The highest will be tried first, /// but if the relevant transaction extension information is not given then we will fall /// back to the next version until we find one we can satisfy. - /// + /// /// All versions returned here should be passable to [`ExtrinsicTypeInfo::extrinsic_extension_info`] /// and return valid extensions information from that. fn extrinsic_extension_version_info( &self, - ) -> Result, ExtrinsicInfoError<'_>>; + ) -> Result, ExtrinsicInfoError<'_>>; } /// An error returned trying to access extrinsic type information. @@ -636,7 +633,11 @@ impl ExtrinsicTypeInfo for frame_metadata::v16::RuntimeMetadataV16 { fn extrinsic_extension_version_info( &self, ) -> Result, ExtrinsicInfoError<'_>> { - Ok(self.extrinsic.transaction_extensions_by_version.keys().copied()) + Ok(self + .extrinsic + .transaction_extensions_by_version + .keys() + .copied()) } } @@ -810,16 +811,18 @@ const _: () = { let calls = as_decoded(calls); - let (call_index, call) = calls.iter().enumerate().find(|(_, c)| { - let name: &str = as_decoded(&c.name).as_ref(); - name == $call_name_arg - }).ok_or_else(|| { - ExtrinsicInfoError::CallNotFoundByName { + let (call_index, call) = calls + .iter() + .enumerate() + .find(|(_, c)| { + let name: &str = as_decoded(&c.name).as_ref(); + name == $call_name_arg + }) + .ok_or_else(|| ExtrinsicInfoError::CallNotFoundByName { call_name: Cow::Owned($call_name_arg.to_string()), pallet_index, pallet_name: Cow::Borrowed(m_name), - } - })?; + })?; let c_name: &str = as_decoded(&call.name).as_ref(); @@ -1058,16 +1061,18 @@ const _: () = { let calls = as_decoded(calls); - let (call_index, call) = calls.iter().enumerate().find(|(_, c)| { - let name: &str = as_decoded(&c.name).as_ref(); - name == call_name - }).ok_or_else(|| { - ExtrinsicInfoError::CallNotFoundByName { + let (call_index, call) = calls + .iter() + .enumerate() + .find(|(_, c)| { + let name: &str = as_decoded(&c.name).as_ref(); + name == call_name + }) + .ok_or_else(|| ExtrinsicInfoError::CallNotFoundByName { call_name: Cow::Owned(call_name.to_string()), pallet_index, pallet_name: Cow::Borrowed(m_name), - } - })?; + })?; let c_name: &str = as_decoded(&call.name).as_ref(); From 42f98aee1acdf0e0664d7a11df4b75f8ee86c60e Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 10 Feb 2026 12:40:25 +0000 Subject: [PATCH 04/12] Expose encode_call_data fns --- src/lib.rs | 5 +- src/methods/extrinsic_encoder.rs | 130 +++++++++++++++++++++++-------- 2 files changed, 102 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2a553ae..5e0986f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,8 +42,9 @@ pub mod extrinsics { pub use crate::methods::extrinsic_encoder::{ ExtrinsicEncodeError, TransactionExtension, TransactionExtensionError, TransactionExtensions, TransactionExtensionsError, - best_v5_general_transaction_extension_version, encode_v4_signed, encode_v4_signed_to, - encode_v4_signed_with_info_to, encode_v4_signer_payload, + best_v5_general_transaction_extension_version, encode_call_data, encode_call_data_to, + encode_call_data_with_info, encode_call_data_with_info_to, encode_v4_signed, + encode_v4_signed_to, encode_v4_signed_with_info_to, encode_v4_signer_payload, encode_v4_signer_payload_with_info, encode_v4_unsigned, encode_v4_unsigned_to, encode_v4_unsigned_with_info_to, encode_v5_bare, encode_v5_bare_to, encode_v5_bare_with_info_to, encode_v5_general, encode_v5_general_to, diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index e00e158..15223cd 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -384,8 +384,7 @@ where .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // And now the actual call data, ie the arguments we're passing to the call - encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner) - .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner)?; // Now, encoding these inner bytes prefixes the compact length to the beginning: encoded_inner.encode_to(out); @@ -486,8 +485,7 @@ where let mut out = Vec::new(); // First encode call data - encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out) - .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) for ext in &ext_info.extension_ids { transaction_extensions @@ -903,8 +901,7 @@ where .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // And now the actual call data, ie the arguments we're passing to the call - encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner) - .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner)?; // Now, encoding these inner bytes prefixes the compact length to the beginning: encoded_inner.encode_to(out); @@ -1015,8 +1012,7 @@ where let mut out = Vec::new(); // First encode call data - encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out) - .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) for ext in &ext_info.extension_ids { @@ -1041,6 +1037,100 @@ where Ok(sp_crypto_hashing::blake2_256(&out)) } +/// Encode the call data for an extrinsic. +/// +/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`]. +pub fn encode_call_data( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + info: &Info, + type_resolver: &Resolver, +) -> Result, ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, +{ + let mut out = Vec::new(); + encode_call_data_to( + pallet_name, + call_name, + call_data, + info, + type_resolver, + &mut out, + )?; + Ok(out) +} + +/// Encode the call data for an extrinsic to the given Vec. +/// +/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`]. +pub fn encode_call_data_to( + pallet_name: &str, + call_name: &str, + call_data: &CallData, + info: &Info, + type_resolver: &Resolver, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + CallData: EncodeAsFields, + Resolver: TypeResolver, + Info: ExtrinsicTypeInfo, +{ + let call_info = info + .extrinsic_call_info_by_name(pallet_name, call_name) + .map_err(|i| i.into_owned()) + .map_err(ExtrinsicEncodeError::CannotGetInfo)?; + + encode_call_data_with_info_to(call_data, &call_info, type_resolver, out) +} + +/// Encode the call data for an extrinsic, given some already-computed [`ExtrinsicCallInfo`]. +/// +/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`]. +pub fn encode_call_data_with_info( + call_data: &CallData, + call_info: &ExtrinsicCallInfo, + type_resolver: &Resolver, +) -> Result, ExtrinsicEncodeError> +where + Resolver: TypeResolver, + CallData: EncodeAsFields, +{ + let mut out = Vec::new(); + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; + Ok(out) +} + +/// Encode the call data for an extrinsic, given some already-computed [`ExtrinsicCallInfo`], +/// to the given Vec. +/// +/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`]. +pub fn encode_call_data_with_info_to( + call_data: &CallData, + call_info: &ExtrinsicCallInfo, + type_resolver: &Resolver, + out: &mut Vec, +) -> Result<(), ExtrinsicEncodeError> +where + Resolver: TypeResolver, + CallData: EncodeAsFields, +{ + let mut fields = call_info.args.iter().map(|arg| Field { + name: Some(&*arg.name), + id: arg.id.clone(), + }); + + call_data + .encode_as_fields_to(&mut fields, type_resolver, out) + .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + + Ok(()) +} + // V4 unsigned and V5 bare extrinsics are basically encoded // in the same way; this helper can do either. fn encode_unsigned_at_version_with_info_to( @@ -1063,8 +1153,7 @@ where call_info.pallet_index.encode_to(&mut out); call_info.call_index.encode_to(&mut out); // Then the arguments for the call: - encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out) - .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; out }; @@ -1079,24 +1168,3 @@ enum TransactionVersion { V4 = 4u8, V5 = 5u8, } - -// Encoding call data is the same for any extrinsic; this method does this part. -fn encode_call_data_with_info_to( - call_data: &CallData, - call_info: &ExtrinsicCallInfo, - type_resolver: &Resolver, - out: &mut Vec, -) -> Result<(), scale_encode::Error> -where - Resolver: TypeResolver, - CallData: EncodeAsFields, -{ - let mut fields = call_info.args.iter().map(|arg| Field { - name: Some(&*arg.name), - id: arg.id.clone(), - }); - - call_data.encode_as_fields_to(&mut fields, type_resolver, out)?; - - Ok(()) -} From 517a3d57f437cd26fe86d4159c5714ad441baadd Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 10 Feb 2026 17:17:18 +0000 Subject: [PATCH 05/12] clippy --- src/methods/extrinsic_encoder.rs | 20 +++++++++---------- .../transaction_extensions.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index 15223cd..27544aa 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -49,8 +49,6 @@ pub enum ExtrinsicEncodeError { CannotFindGoodExtensionVersion, } -//// V4 - /// Encode a V4 unsigned extrinsic (also known as an inherent). /// /// This is the same as [`encode_v4_unsigned_to`], but returns the encoded extrinsic as a `Vec`, @@ -173,7 +171,7 @@ where { encode_unsigned_at_version_with_info_to( call_data, - &call_info, + call_info, type_resolver, TransactionVersion::V4, out, @@ -217,6 +215,7 @@ where /// &metadata.types, /// ).unwrap(); /// ``` +#[allow(clippy::too_many_arguments)] pub fn encode_v4_signed( pallet_name: &str, call_name: &str, @@ -287,6 +286,7 @@ where /// &mut encoded, /// ).unwrap(); /// ``` +#[allow(clippy::too_many_arguments)] pub fn encode_v4_signed_to( pallet_name: &str, call_name: &str, @@ -339,6 +339,7 @@ where /// Unlike [`encode_v4_signed_to`], which obtains the call, signature, and extension info internally /// given the pallet and call names, this function takes these as arguments. This is useful if you /// already have this information available, for example if you are encoding multiple extrinsics. +#[allow(clippy::too_many_arguments)] pub fn encode_v4_signed_with_info_to( call_data: &CallData, transaction_extensions: &Exts, @@ -485,7 +486,7 @@ where let mut out = Vec::new(); // First encode call data - encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) for ext in &ext_info.extension_ids { transaction_extensions @@ -511,8 +512,6 @@ where Ok(out) } -//// V5 - /// Encode a V5 bare extrinsic (also known as an inherent), ready to submit. /// /// V5 bare extrinsics contain only call data with no transaction extensions or signature. @@ -638,7 +637,7 @@ where { encode_unsigned_at_version_with_info_to( call_data, - &call_info, + call_info, type_resolver, TransactionVersion::V5, out, @@ -832,6 +831,7 @@ where /// &mut encoded, /// ).unwrap(); /// ``` +#[allow(clippy::too_many_arguments)] pub fn encode_v5_general_to( pallet_name: &str, call_name: &str, @@ -858,7 +858,7 @@ where .map_err(|i| i.into_owned()) .map_err(ExtrinsicEncodeError::CannotGetInfo)?; - return encode_v5_general_with_info_to( + encode_v5_general_with_info_to( call_data, transaction_extension_version, transaction_extensions, @@ -866,7 +866,7 @@ where &call_info, &ext_info, out, - ); + ) } /// Encode a V5 general extrinsic to a provided output buffer, using pre-computed type information. @@ -1012,7 +1012,7 @@ where let mut out = Vec::new(); // First encode call data - encode_call_data_with_info_to(call_data, &call_info, type_resolver, &mut out)?; + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) for ext in &ext_info.extension_ids { diff --git a/src/methods/extrinsic_encoder/transaction_extensions.rs b/src/methods/extrinsic_encoder/transaction_extensions.rs index ed25951..de02f5a 100644 --- a/src/methods/extrinsic_encoder/transaction_extensions.rs +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -93,7 +93,7 @@ impl TransactionExtensions for [&dyn TransactionExtension] { fn contains_extension(&self, name: &str) -> bool { - self.iter().find(|e| e.extension_name() == name).is_some() + self.iter().any(|e| e.extension_name() == name) } fn encode_extension_value_to( From 7886882c733a3cec1602598c447e28b22811dc3e Mon Sep 17 00:00:00 2001 From: James Wilson Date: Wed, 11 Feb 2026 16:01:46 +0000 Subject: [PATCH 06/12] Add implicit IDs to info, check empty types to ignore exts we don't care about --- src/lib.rs | 4 +- src/methods/extrinsic_encoder.rs | 86 ++++++++++++++++--- .../transaction_extensions.rs | 9 +- src/methods/extrinsic_type_info.rs | 56 ++++++++---- 4 files changed, 122 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5e0986f..71b1d3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,8 +52,8 @@ pub mod extrinsics { encode_v5_signer_payload_with_info, }; pub use crate::methods::extrinsic_type_info::{ - ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicInfoArg, ExtrinsicInfoError, - ExtrinsicSignatureInfo, ExtrinsicTypeInfo, + ExtrinsicCallInfo, ExtrinsicCallInfoArg, ExtrinsicExtensionInfo, ExtrinsicExtensionInfoArg, + ExtrinsicInfoError, ExtrinsicSignatureInfo, ExtrinsicTypeInfo, }; } diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index 27544aa..e901040 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -896,9 +896,16 @@ where transaction_extension_version.encode_to(&mut encoded_inner); // Transaction Extensions next. These may include a signature/address. for ext in &ext_info.extension_ids { - transaction_extensions - .encode_extension_value_to(&ext.name, ext.id.clone(), type_resolver, &mut encoded_inner) - .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + if !is_type_empty(ext.id.clone(), type_resolver) { + transaction_extensions + .encode_extension_value_to( + &ext.name, + ext.id.clone(), + type_resolver, + &mut encoded_inner, + ) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } } // And now the actual call data, ie the arguments we're passing to the call encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner)?; @@ -1016,21 +1023,30 @@ where // Then the signer payload value (ie roughly the bytes that will appear in the tx) for ext in &ext_info.extension_ids { - transaction_extensions - .encode_extension_value_for_signer_payload_to( - &ext.name, - ext.id.clone(), - type_resolver, - &mut out, - ) - .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + if !is_type_empty(ext.id.clone(), type_resolver) { + transaction_extensions + .encode_extension_value_for_signer_payload_to( + &ext.name, + ext.id.clone(), + type_resolver, + &mut out, + ) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } } // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) for ext in &ext_info.extension_ids { - transaction_extensions - .encode_extension_implicit_to(&ext.name, ext.id.clone(), type_resolver, &mut out) - .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + if !is_type_empty(ext.id.clone(), type_resolver) { + transaction_extensions + .encode_extension_implicit_to( + &ext.name, + ext.implicit_id.clone(), + type_resolver, + &mut out, + ) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; + } } // Finally hash it (regardless of length). @@ -1168,3 +1184,45 @@ enum TransactionVersion { V4 = 4u8, V5 = 5u8, } + +/// Checks to see whether the type being given is empty, ie would require +/// 0 bytes to encode. We use this to skip 0 byte trasnsaction extensions; ones +/// that are mentioned in the metadata but only used in the node side and require +/// no bytes to be given. +fn is_type_empty(type_id: Resolver::TypeId, types: &Resolver) -> bool { + struct IsEmptyVisitor<'r, R> { + types: &'r R, + } + impl<'r, R: TypeResolver> scale_type_resolver::ResolvedTypeVisitor<'r> for IsEmptyVisitor<'r, R> { + type TypeId = R::TypeId; + type Value = bool; + + // The default ans safe assumption is that a type is _not_ empty. + fn visit_unhandled(self, _: scale_type_resolver::UnhandledKind) -> Self::Value { + false + } + // Arrays are empty if they are 0 length or the type inside is empty. + fn visit_array(self, type_id: Self::TypeId, len: usize) -> Self::Value { + len == 0 || is_type_empty(type_id, self.types) + } + // Composites are empty if all of their fields are empty. + fn visit_composite(self, _path: Path, mut fields: Fields) -> Self::Value + where + Path: scale_type_resolver::PathIter<'r>, + Fields: scale_decode::FieldIter<'r, Self::TypeId>, + { + fields.all(|f| is_type_empty(f.id, self.types)) + } + // Tuples are empty if all of their fields are empty. + fn visit_tuple(self, mut type_ids: TypeIds) -> Self::Value + where + TypeIds: ExactSizeIterator, + { + type_ids.all(|id| is_type_empty(id, self.types)) + } + } + + types + .resolve_type(type_id, IsEmptyVisitor { types }) + .unwrap_or_default() +} diff --git a/src/methods/extrinsic_encoder/transaction_extensions.rs b/src/methods/extrinsic_encoder/transaction_extensions.rs index de02f5a..26efc96 100644 --- a/src/methods/extrinsic_encoder/transaction_extensions.rs +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -342,6 +342,11 @@ const _: () = { impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16); impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17); impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19); - impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19, V 20); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23, Y 24); + impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23, Y 24, Z 25); }; diff --git a/src/methods/extrinsic_type_info.rs b/src/methods/extrinsic_type_info.rs index 0cdd8b0..e57040d 100644 --- a/src/methods/extrinsic_type_info.rs +++ b/src/methods/extrinsic_type_info.rs @@ -274,11 +274,13 @@ impl ExtrinsicInfoError<'_> { /// An argument with a name and type ID. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtrinsicInfoArg<'info, TypeId> { +pub struct ExtrinsicExtensionInfoArg<'info, TypeId> { /// Argument name. pub name: Cow<'info, str>, /// Argument type ID. pub id: TypeId, + /// The type ID for implicit arguments. + pub implicit_id: TypeId, } /// Extrinsic call data information given pallet and call names. @@ -293,7 +295,16 @@ pub struct ExtrinsicCallInfo<'info, TypeId> { /// Name of the call. pub call_name: Cow<'info, str>, /// Names and types of each of the extrinsic arguments. - pub args: Vec>, + pub args: Vec>, +} + +/// An argument in some extrinsic call data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtrinsicCallInfoArg<'info, TypeId> { + /// Argument name. + pub name: Cow<'info, str>, + /// Argument type ID. + pub id: TypeId, } /// Extrinsic signature information. @@ -309,7 +320,7 @@ pub struct ExtrinsicSignatureInfo { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtrinsicExtensionInfo<'info, TypeId> { /// Names and type IDs of the transaction extensions. - pub extension_ids: Vec>, + pub extension_ids: Vec>, } macro_rules! impl_call_info_by_name_body_for_v14_to_v16 { @@ -378,7 +389,7 @@ macro_rules! impl_call_info_by_name_body_for_v14_to_v16 { .as_ref() .map(|n| Cow::Borrowed(&**n)) .unwrap_or(Cow::Owned(String::new())); - ExtrinsicInfoArg { id, name } + ExtrinsicCallInfoArg { id, name } }) .collect(); @@ -456,7 +467,7 @@ macro_rules! impl_call_info_by_index_body_for_v14_to_v16 { .as_ref() .map(|n| Cow::Borrowed(&**n)) .unwrap_or(Cow::Owned(String::new())); - ExtrinsicInfoArg { id, name } + ExtrinsicCallInfoArg { id, name } }) .collect(); @@ -506,8 +517,9 @@ impl ExtrinsicTypeInfo for frame_metadata::v14::RuntimeMetadataV14 { .extrinsic .signed_extensions .iter() - .map(|e| ExtrinsicInfoArg { + .map(|e| ExtrinsicExtensionInfoArg { id: e.ty.id, + implicit_id: e.additional_signed.id, name: Cow::Borrowed(&e.identifier), }) .collect(); @@ -555,8 +567,9 @@ impl ExtrinsicTypeInfo for frame_metadata::v15::RuntimeMetadataV15 { .extrinsic .signed_extensions .iter() - .map(|e| ExtrinsicInfoArg { + .map(|e| ExtrinsicExtensionInfoArg { id: e.ty.id, + implicit_id: e.additional_signed.id, name: Cow::Borrowed(&e.identifier), }) .collect(); @@ -621,8 +634,9 @@ impl ExtrinsicTypeInfo for frame_metadata::v16::RuntimeMetadataV16 { .get(idx.0 as usize) .expect("Index in transaction_extensions_by_version should exist in transaction_extensions"); - ExtrinsicInfoArg { + ExtrinsicExtensionInfoArg { id: ext.ty.id, + implicit_id: ext.implicit.id, name: Cow::Borrowed(&ext.identifier), } }) @@ -712,6 +726,7 @@ fn err_if_bad_extension_version<'a>( #[cfg(feature = "legacy")] const _: () = { use crate::utils::as_decoded; + use alloc::format; use scale_info_legacy::LookupName; macro_rules! impl_extrinsic_info_by_index_body_for_v8_to_v11 { @@ -759,7 +774,7 @@ const _: () = { let ty = as_decoded(&a.ty); let id = parse_lookup_name(ty)?.in_pallet(m_name); let name = as_decoded(&a.name); - Ok(ExtrinsicInfoArg { + Ok(ExtrinsicCallInfoArg { id, name: Cow::Borrowed(name), }) @@ -834,7 +849,7 @@ const _: () = { let ty: &str = as_decoded(&a.ty).as_ref(); let id = parse_lookup_name(ty)?.in_pallet(m_name); let name: &str = as_decoded(&a.name).as_ref(); - Ok(ExtrinsicInfoArg { + Ok(ExtrinsicCallInfoArg { id, name: Cow::Borrowed(name), }) @@ -884,9 +899,12 @@ const _: () = { err_if_bad_extension_version(extension_version)?; Ok(ExtrinsicExtensionInfo { - extension_ids: Vec::from_iter([ExtrinsicInfoArg { + extension_ids: Vec::from_iter([ExtrinsicExtensionInfoArg { name: Cow::Borrowed("ExtrinsicSignedExtensions"), id: parse_lookup_name("hardcoded::ExtrinsicSignedExtensions")?, + implicit_id: parse_lookup_name( + "hardcoded::ExtrinsicSignedExtensionsImplicit", + )?, }]), }) } @@ -942,9 +960,13 @@ const _: () = { .map(|e| { let signed_ext_name = as_decoded(e); let signed_ext_id = parse_lookup_name(signed_ext_name)?; + let signed_ext_implicit_id = + parse_lookup_name(&format!("{signed_ext_name}Implicit")) + .map_err(|e| e.into_owned())?; - Ok(ExtrinsicInfoArg { + Ok(ExtrinsicExtensionInfoArg { id: signed_ext_id, + implicit_id: signed_ext_implicit_id, name: Cow::Borrowed(signed_ext_name), }) }) @@ -1010,7 +1032,7 @@ const _: () = { let ty = as_decoded(&a.ty); let id = parse_lookup_name(ty)?.in_pallet(m_name); let name = as_decoded(&a.name); - Ok(ExtrinsicInfoArg { + Ok(ExtrinsicCallInfoArg { id, name: Cow::Borrowed(name), }) @@ -1084,7 +1106,7 @@ const _: () = { let ty: &str = as_decoded(&a.ty).as_ref(); let id = parse_lookup_name(ty)?.in_pallet(m_name); let name: &str = as_decoded(&a.name).as_ref(); - Ok(ExtrinsicInfoArg { + Ok(ExtrinsicCallInfoArg { id, name: Cow::Borrowed(name), }) @@ -1122,9 +1144,13 @@ const _: () = { .map(|e| { let signed_ext_name = as_decoded(e); let signed_ext_id = parse_lookup_name(signed_ext_name)?; + let signed_ext_implicit_id = + parse_lookup_name(&format!("{signed_ext_name}Implicit")) + .map_err(|e| e.into_owned())?; - Ok(ExtrinsicInfoArg { + Ok(ExtrinsicExtensionInfoArg { id: signed_ext_id, + implicit_id: signed_ext_implicit_id, name: Cow::Borrowed(signed_ext_name), }) }) From 0e37a69848af3b896db4cf13d9fd72c7652f9ded Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 12 Feb 2026 10:34:54 +0000 Subject: [PATCH 07/12] Make transaction extension names consts and remove slice impl --- .../transaction_extension.rs | 2 +- .../transaction_extensions.rs | 108 +----------------- src/methods/storage_type_info.rs | 2 +- src/utils.rs | 2 + src/utils/type_registry_from_metadata.rs | 4 - 5 files changed, 9 insertions(+), 109 deletions(-) diff --git a/src/methods/extrinsic_encoder/transaction_extension.rs b/src/methods/extrinsic_encoder/transaction_extension.rs index 1fca06a..ac5e266 100644 --- a/src/methods/extrinsic_encoder/transaction_extension.rs +++ b/src/methods/extrinsic_encoder/transaction_extension.rs @@ -22,7 +22,7 @@ use scale_type_resolver::TypeResolver; /// explicit `value` bytes to a transaction, or "implicit" bytes to a transaction signer payload. pub trait TransactionExtension { /// The name of this transaction extension. - fn extension_name(&self) -> &'static str; + const NAME: &str; /// Given type information for the expected transaction extension, /// this should encode the value (ie the bytes that will appear in the diff --git a/src/methods/extrinsic_encoder/transaction_extensions.rs b/src/methods/extrinsic_encoder/transaction_extensions.rs index 26efc96..d65ea69 100644 --- a/src/methods/extrinsic_encoder/transaction_extensions.rs +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -20,8 +20,7 @@ use alloc::vec::Vec; use scale_type_resolver::TypeResolver; /// This trait can be implemented for anything which represents a set of transaction extensions. -/// It's implemented by default for tuples of items which implement [`TransactionExtension`], -/// and for slices of `&dyn TransactionExtension`. +/// It's implemented by default for tuples of items which implement [`TransactionExtension`]. pub trait TransactionExtensions { /// Is a given transaction extension contained within this set? fn contains_extension(&self, name: &str) -> bool; @@ -87,103 +86,6 @@ pub enum TransactionExtensionsError { }, } -// `TransactionExtension` is object safe and so `TransactionExtensions` can be implemented -// for slices of `&dyn TransactionExtension`s. -impl TransactionExtensions - for [&dyn TransactionExtension] -{ - fn contains_extension(&self, name: &str) -> bool { - self.iter().any(|e| e.extension_name() == name) - } - - fn encode_extension_value_to( - &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, - out: &mut Vec, - ) -> Result<(), TransactionExtensionsError> { - let len = out.len(); - for ext in self { - if ext.extension_name() == name { - return ext - .encode_value_to(type_id, type_resolver, out) - .map_err(|e| { - // Protection: if we are returning an error then - // no bytes should have been encoded to the given - // Vec. Ensure that this is true: - while out.len() > len { - out.pop(); - } - TransactionExtensionsError::Other { - extension_name: name.to_owned(), - error: e, - } - }); - } - } - Err(TransactionExtensionsError::NotFound(name.to_owned())) - } - - fn encode_extension_value_for_signer_payload_to( - &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, - out: &mut Vec, - ) -> Result<(), TransactionExtensionsError> { - let len = out.len(); - for ext in self { - if ext.extension_name() == name { - return ext - .encode_value_for_signer_payload_to(type_id, type_resolver, out) - .map_err(|e| { - // Protection: if we are returning an error then - // no bytes should have been encoded to the given - // Vec. Ensure that this is true: - while out.len() > len { - out.pop(); - } - TransactionExtensionsError::Other { - extension_name: name.to_owned(), - error: e, - } - }); - } - } - Err(TransactionExtensionsError::NotFound(name.to_owned())) - } - - fn encode_extension_implicit_to( - &self, - name: &str, - type_id: ::TypeId, - type_resolver: &Resolver, - out: &mut Vec, - ) -> Result<(), TransactionExtensionsError> { - let len = out.len(); - for ext in self { - if ext.extension_name() == name { - return ext - .encode_implicit_to(type_id, type_resolver, out) - .map_err(|e| { - // Protection: if we are returning an error then - // no bytes should have been encoded to the given - // Vec. Ensure that this is true: - while out.len() > len { - out.pop(); - } - TransactionExtensionsError::Other { - extension_name: name.to_owned(), - error: e, - } - }); - } - } - Err(TransactionExtensionsError::NotFound(name.to_owned())) - } -} - // Empty tuples impl `TransactionExtensions`: if called they emit a not found error. impl TransactionExtensions for () { fn contains_extension(&self, _name: &str) -> bool { @@ -221,7 +123,7 @@ macro_rules! impl_tuples { { fn contains_extension(&self, name: &str) -> bool { $( - if self.$index.extension_name() == name { + if $ident::NAME == name { return true } )* @@ -238,7 +140,7 @@ macro_rules! impl_tuples { let len = out.len(); $( - if self.$index.extension_name() == name { + if $ident::NAME == name { return self.$index.encode_value_to(type_id, type_resolver, out) .map_err(|e| { // Protection: if we are returning an error then @@ -268,7 +170,7 @@ macro_rules! impl_tuples { let len = out.len(); $( - if self.$index.extension_name() == name { + if $ident::NAME == name { return self.$index.encode_value_for_signer_payload_to(type_id, type_resolver, out) .map_err(|e| { // Protection: if we are returning an error then @@ -298,7 +200,7 @@ macro_rules! impl_tuples { let len = out.len(); $( - if self.$index.extension_name() == name { + if $ident::NAME == name { return self.$index.encode_implicit_to(type_id, type_resolver, out) .map_err(|e| { // Protection: if we are returning an error then diff --git a/src/methods/storage_type_info.rs b/src/methods/storage_type_info.rs index c4b7282..d2e9397 100644 --- a/src/methods/storage_type_info.rs +++ b/src/methods/storage_type_info.rs @@ -17,7 +17,6 @@ use super::Entry; use crate::utils::Either; use alloc::borrow::Cow; use alloc::borrow::ToOwned; -use alloc::format; use alloc::string::String; use alloc::vec::Vec; @@ -462,6 +461,7 @@ to_latest_storage_hasher!(to_storage_hasher_v16, frame_metadata::v16::StorageHas mod legacy { use super::*; use crate::utils::as_decoded; + use alloc::format; use frame_metadata::decode_different::DecodeDifferent; use scale_info_legacy::LookupName; diff --git a/src/utils.rs b/src/utils.rs index fa516d4..a962f23 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -17,6 +17,7 @@ mod decodable_values; mod decode_with_error_tracing; mod either; mod encodable_values; +#[cfg(feature = "legacy")] mod type_registry_from_metadata; pub use decodable_values::{DecodableValues, IntoDecodableValues}; @@ -24,6 +25,7 @@ pub use encodable_values::{EncodableValues, IntoEncodableValues}; pub use decode_with_error_tracing::{DecodeErrorTrace, decode_with_error_tracing}; pub use either::Either; + #[cfg(feature = "legacy")] pub use type_registry_from_metadata::{ ToTypeRegistry, type_registry_from_metadata, type_registry_from_metadata_any, diff --git a/src/utils/type_registry_from_metadata.rs b/src/utils/type_registry_from_metadata.rs index 530b891..8e4cd87 100644 --- a/src/utils/type_registry_from_metadata.rs +++ b/src/utils/type_registry_from_metadata.rs @@ -24,7 +24,6 @@ /// /// - `builtin::module::event::$PALLET` - A variant containing the events in a specific pallet. /// - `builtin::module::call::$PALLET` - A variant containing the calls in a specific pallet. -#[cfg(feature = "legacy")] pub fn type_registry_from_metadata( metadata: &Md, ) -> Result { @@ -33,7 +32,6 @@ pub fn type_registry_from_metadata( /// This is like [`type_registry_from_metadata`], except it can be handed the outer [`frame_metadata::RuntimeMetadata`] /// enum and will extract types from it where appropriate (handing back no types for deprecated or modern metadata). -#[cfg(feature = "legacy")] pub fn type_registry_from_metadata_any( metadata: &frame_metadata::RuntimeMetadata, ) -> Result { @@ -59,7 +57,6 @@ pub fn type_registry_from_metadata_any( } } -#[cfg(feature = "legacy")] /// This is used with the [`type_registry_from_metadata`] helper function to extract types from the /// metadata. It is not intended to be implemented on anything else. pub trait ToTypeRegistry: sealed::Sealed { @@ -73,7 +70,6 @@ mod sealed { pub trait Sealed {} } -#[cfg(feature = "legacy")] const _: () = { use super::as_decoded; use alloc::borrow::ToOwned; From 12dd787f401a88dfcab28c040883d0802d0a15c1 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 12 Feb 2026 16:33:03 +0000 Subject: [PATCH 08/12] Fix call data: need to encode pallet and call index here --- .gitignore | 1 + src/methods/extrinsic_encoder.rs | 149 ++++++++++++++++++++----------- 2 files changed, 99 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..80f4384 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.claude \ No newline at end of file diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index e901040..84e3016 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -15,7 +15,6 @@ mod transaction_extension; mod transaction_extensions; - use super::extrinsic_type_info::{ ExtrinsicCallInfo, ExtrinsicExtensionInfo, ExtrinsicInfoError, ExtrinsicSignatureInfo, ExtrinsicTypeInfo, @@ -360,8 +359,10 @@ where { // Encode the "inner" bytes let mut encoded_inner = Vec::new(); + // "is signed" + transaction protocol version (4) (0b10000000 + 4u8).encode_to(&mut encoded_inner); + // Who is this transaction from (corresponds to public key of signature) address .encode_as_type_to( @@ -370,6 +371,7 @@ where &mut encoded_inner, ) .map_err(ExtrinsicEncodeError::CannotEncodeAddress)?; + // Signature for the above identity signature .encode_as_type_to( @@ -378,12 +380,19 @@ where &mut encoded_inner, ) .map_err(ExtrinsicEncodeError::CannotEncodeSignature)?; + // Signed extensions (now Transaction Extensions) - for ext in &ext_info.extension_ids { + for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { transaction_extensions - .encode_extension_value_to(&ext.name, ext.id.clone(), type_resolver, &mut encoded_inner) + .encode_extension_value_to( + name, + id, + type_resolver, + &mut encoded_inner, + ) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } + // And now the actual call data, ie the arguments we're passing to the call encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner)?; @@ -487,23 +496,31 @@ where // First encode call data encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; + // Then the signer payload value (ie roughly the bytes that will appear in the tx) - for ext in &ext_info.extension_ids { + for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { transaction_extensions .encode_extension_value_for_signer_payload_to( - &ext.name, - ext.id.clone(), + name, + id, type_resolver, &mut out, ) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } + // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) - for ext in &ext_info.extension_ids { + for (name, id) in iter_nonempty_extention_implicits(ext_info, type_resolver) { transaction_extensions - .encode_extension_implicit_to(&ext.name, ext.id.clone(), type_resolver, &mut out) + .encode_extension_implicit_to( + name, + id, + type_resolver, + &mut out, + ) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } + // Finally we need to hash it if it's too long if out.len() > 256 { out = sp_crypto_hashing::blake2_256(&out).to_vec(); @@ -690,11 +707,13 @@ where pub fn best_v5_general_transaction_extension_version( transaction_extensions: &Exts, info: &Info, + type_resolver: &Resolver, ) -> Result where Info: ExtrinsicTypeInfo, Exts: TransactionExtensions, Resolver: TypeResolver, + Info::TypeId: Clone, { let extension_versions = info .extrinsic_extension_version_info() @@ -712,7 +731,11 @@ where let have_data = ext_info .extension_ids .iter() - .all(|e| transaction_extensions.contains_extension(&e.name)); + .all(|e| { + let is_value_empty = is_type_empty(e.id.clone(), type_resolver); + let is_implicit_empty = is_type_empty(e.implicit_id.clone(), type_resolver); + (is_value_empty && is_implicit_empty) || transaction_extensions.contains_extension(&e.name) + }); // If we have all of the data we need, encode the extrinsic, // else loop and try the next extension version. @@ -890,23 +913,25 @@ where { // Encode the "inner" bytes let mut encoded_inner = Vec::new(); + // "is signed" + transaction protocol version (4) (0b01000000 + 5u8).encode_to(&mut encoded_inner); + // Version of the transaction extensions. transaction_extension_version.encode_to(&mut encoded_inner); - // Transaction Extensions next. These may include a signature/address. - for ext in &ext_info.extension_ids { - if !is_type_empty(ext.id.clone(), type_resolver) { - transaction_extensions - .encode_extension_value_to( - &ext.name, - ext.id.clone(), - type_resolver, - &mut encoded_inner, - ) - .map_err(ExtrinsicEncodeError::TransactionExtensions)?; - } + + // Transaction Extensions next. These may include a signature/address + for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { + transaction_extensions + .encode_extension_value_to( + name, + id, + type_resolver, + &mut encoded_inner, + ) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } + // And now the actual call data, ie the arguments we're passing to the call encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut encoded_inner)?; @@ -1022,31 +1047,27 @@ where encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) - for ext in &ext_info.extension_ids { - if !is_type_empty(ext.id.clone(), type_resolver) { - transaction_extensions - .encode_extension_value_for_signer_payload_to( - &ext.name, - ext.id.clone(), - type_resolver, - &mut out, - ) - .map_err(ExtrinsicEncodeError::TransactionExtensions)?; - } + for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { + transaction_extensions + .encode_extension_value_for_signer_payload_to( + name, + id, + type_resolver, + &mut out, + ) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) - for ext in &ext_info.extension_ids { - if !is_type_empty(ext.id.clone(), type_resolver) { - transaction_extensions - .encode_extension_implicit_to( - &ext.name, - ext.implicit_id.clone(), - type_resolver, - &mut out, - ) - .map_err(ExtrinsicEncodeError::TransactionExtensions)?; - } + for (name, id) in iter_nonempty_extention_implicits(ext_info, type_resolver) { + transaction_extensions + .encode_extension_implicit_to( + name, + id, + type_resolver, + &mut out, + ) + .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // Finally hash it (regardless of length). @@ -1082,7 +1103,8 @@ where /// Encode the call data for an extrinsic to the given Vec. /// -/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`]. +/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`], but +/// with a byte for the pallet index and call index prepended. pub fn encode_call_data_to( pallet_name: &str, call_name: &str, @@ -1106,7 +1128,8 @@ where /// Encode the call data for an extrinsic, given some already-computed [`ExtrinsicCallInfo`]. /// -/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`]. +/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`], but +/// with a byte for the pallet index and call index prepended. pub fn encode_call_data_with_info( call_data: &CallData, call_info: &ExtrinsicCallInfo, @@ -1124,22 +1147,27 @@ where /// Encode the call data for an extrinsic, given some already-computed [`ExtrinsicCallInfo`], /// to the given Vec. /// -/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`]. +/// This is basically an alias for [`scale_encode::EncodeAsFields::encode_as_fields()`], but +/// with a byte for the pallet index and call index prepended. pub fn encode_call_data_with_info_to( call_data: &CallData, call_info: &ExtrinsicCallInfo, type_resolver: &Resolver, - out: &mut Vec, + mut out: &mut Vec, ) -> Result<(), ExtrinsicEncodeError> where Resolver: TypeResolver, CallData: EncodeAsFields, { + // Pallet and call index to identify the call: + call_info.pallet_index.encode_to(&mut out); + call_info.call_index.encode_to(&mut out); + + // Arguments to this call: let mut fields = call_info.args.iter().map(|arg| Field { name: Some(&*arg.name), id: arg.id.clone(), }); - call_data .encode_as_fields_to(&mut fields, type_resolver, out) .map_err(ExtrinsicEncodeError::CannotEncodeCallData)?; @@ -1165,9 +1193,6 @@ where let mut out = Vec::new(); // Transaction version (4): (tx_version as u8).encode_to(&mut out); - // Pallet and call index to identify the call: - call_info.pallet_index.encode_to(&mut out); - call_info.call_index.encode_to(&mut out); // Then the arguments for the call: encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; out @@ -1185,6 +1210,28 @@ enum TransactionVersion { V5 = 5u8, } +/// Iterate over the non-empty extension implicit name/IDs +fn iter_nonempty_extention_implicits<'exts, 'info, Resolver: TypeResolver>( + extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, + types: &Resolver +) -> impl Iterator { + extension_info.extension_ids + .iter() + .filter(|arg| !is_type_empty(arg.implicit_id.clone(), types)) + .map(|arg| (&*arg.name, arg.implicit_id.clone())) +} + +/// Iterate over the non-empty extension value name/IDs +fn iter_nonempty_extention_values<'exts, 'info, Resolver: TypeResolver>( + extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, + types: &Resolver +) -> impl Iterator { + extension_info.extension_ids + .iter() + .filter(|arg| !is_type_empty(arg.id.clone(), types)) + .map(|arg| (&*arg.name, arg.id.clone())) +} + /// Checks to see whether the type being given is empty, ie would require /// 0 bytes to encode. We use this to skip 0 byte trasnsaction extensions; ones /// that are mentioned in the metadata but only used in the node side and require From 8ab6cc4a090d2f231036e823adfe0db355b67936 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 12 Feb 2026 16:33:44 +0000 Subject: [PATCH 09/12] fmt --- src/methods/extrinsic_encoder.rs | 70 +++++++++----------------------- 1 file changed, 20 insertions(+), 50 deletions(-) diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index 84e3016..f66c621 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -384,12 +384,7 @@ where // Signed extensions (now Transaction Extensions) for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { transaction_extensions - .encode_extension_value_to( - name, - id, - type_resolver, - &mut encoded_inner, - ) + .encode_extension_value_to(name, id, type_resolver, &mut encoded_inner) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } @@ -500,24 +495,14 @@ where // Then the signer payload value (ie roughly the bytes that will appear in the tx) for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { transaction_extensions - .encode_extension_value_for_signer_payload_to( - name, - id, - type_resolver, - &mut out, - ) + .encode_extension_value_for_signer_payload_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) for (name, id) in iter_nonempty_extention_implicits(ext_info, type_resolver) { transaction_extensions - .encode_extension_implicit_to( - name, - id, - type_resolver, - &mut out, - ) + .encode_extension_implicit_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } @@ -728,14 +713,12 @@ where .map_err(ExtrinsicEncodeError::CannotGetInfo)?; // Do we have all of the extension data for this version? - let have_data = ext_info - .extension_ids - .iter() - .all(|e| { - let is_value_empty = is_type_empty(e.id.clone(), type_resolver); - let is_implicit_empty = is_type_empty(e.implicit_id.clone(), type_resolver); - (is_value_empty && is_implicit_empty) || transaction_extensions.contains_extension(&e.name) - }); + let have_data = ext_info.extension_ids.iter().all(|e| { + let is_value_empty = is_type_empty(e.id.clone(), type_resolver); + let is_implicit_empty = is_type_empty(e.implicit_id.clone(), type_resolver); + (is_value_empty && is_implicit_empty) + || transaction_extensions.contains_extension(&e.name) + }); // If we have all of the data we need, encode the extrinsic, // else loop and try the next extension version. @@ -923,12 +906,7 @@ where // Transaction Extensions next. These may include a signature/address for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { transaction_extensions - .encode_extension_value_to( - name, - id, - type_resolver, - &mut encoded_inner, - ) + .encode_extension_value_to(name, id, type_resolver, &mut encoded_inner) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } @@ -1049,24 +1027,14 @@ where // Then the signer payload value (ie roughly the bytes that will appear in the tx) for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { transaction_extensions - .encode_extension_value_for_signer_payload_to( - name, - id, - type_resolver, - &mut out, - ) + .encode_extension_value_for_signer_payload_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) for (name, id) in iter_nonempty_extention_implicits(ext_info, type_resolver) { transaction_extensions - .encode_extension_implicit_to( - name, - id, - type_resolver, - &mut out, - ) + .encode_extension_implicit_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } @@ -1212,10 +1180,11 @@ enum TransactionVersion { /// Iterate over the non-empty extension implicit name/IDs fn iter_nonempty_extention_implicits<'exts, 'info, Resolver: TypeResolver>( - extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, - types: &Resolver + extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, + types: &Resolver, ) -> impl Iterator { - extension_info.extension_ids + extension_info + .extension_ids .iter() .filter(|arg| !is_type_empty(arg.implicit_id.clone(), types)) .map(|arg| (&*arg.name, arg.implicit_id.clone())) @@ -1223,10 +1192,11 @@ fn iter_nonempty_extention_implicits<'exts, 'info, Resolver: TypeResolver>( /// Iterate over the non-empty extension value name/IDs fn iter_nonempty_extention_values<'exts, 'info, Resolver: TypeResolver>( - extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, - types: &Resolver + extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, + types: &Resolver, ) -> impl Iterator { - extension_info.extension_ids + extension_info + .extension_ids .iter() .filter(|arg| !is_type_empty(arg.id.clone(), types)) .map(|arg| (&*arg.name, arg.id.clone())) From 68138ac19ecda500dd693a13c3f3d14a7cf08fc6 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 12 Feb 2026 16:38:33 +0000 Subject: [PATCH 10/12] Bump to 0.17 and small no-std/wasm fix --- CHANGELOG.md | 4 ++++ Cargo.lock | 4 ++-- Cargo.toml | 2 +- src/methods/extrinsic_encoder.rs | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 854e064..d3fe85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog]. [Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ +## 0.17.0 (2025-12-03) + +- Add extrinsic encode methods in `frame_decode::extrinsics`. This covers encoding v4 and v5 unsigned and signed extrinsics, new traits for providing transaction extensions, encoding v4 and v5 signer payloads and encoding call data. + ## 0.16.1 (2025-12-03) - Expose the `crate::helpers::ToTypeRegistry` trait so that inputs to `crate::helpers::type_registry_from_metadata` can be named. Make it sealed so that others cannot rely on it. diff --git a/Cargo.lock b/Cargo.lock index aaed1d3..12bddad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,7 +790,7 @@ dependencies = [ [[package]] name = "frame-decode" -version = "0.16.1" +version = "0.17.0" dependencies = [ "frame-metadata 23.0.1", "hex", @@ -811,7 +811,7 @@ dependencies = [ name = "frame-decode-tester" version = "0.1.0" dependencies = [ - "frame-decode 0.16.1", + "frame-decode 0.17.0", "frame-metadata 23.0.1", "hex", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index 8d2a302..ebebf35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frame-decode" -version = "0.16.1" +version = "0.17.0" edition = "2024" description = "Decode extrinsics and storage from Substrate based chains" license = "Apache-2.0" diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index f66c621..09ff7b6 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -1121,15 +1121,15 @@ pub fn encode_call_data_with_info_to( call_data: &CallData, call_info: &ExtrinsicCallInfo, type_resolver: &Resolver, - mut out: &mut Vec, + out: &mut Vec, ) -> Result<(), ExtrinsicEncodeError> where Resolver: TypeResolver, CallData: EncodeAsFields, { // Pallet and call index to identify the call: - call_info.pallet_index.encode_to(&mut out); - call_info.call_index.encode_to(&mut out); + call_info.pallet_index.encode_to(out); + call_info.call_index.encode_to(out); // Arguments to this call: let mut fields = call_info.args.iter().map(|arg| Field { From 305beeb05f995ee322197977d61228cf5a3084e0 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 12 Feb 2026 17:34:18 +0000 Subject: [PATCH 11/12] Address copilot comments --- src/methods/extrinsic_encoder.rs | 22 +++++++++---------- .../transaction_extensions.rs | 12 +++------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index 09ff7b6..36978b6 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -43,7 +43,7 @@ pub enum ExtrinsicEncodeError { #[error("Extrinsic encoding failed: cannot encode transaction extensions: {0}")] TransactionExtensions(TransactionExtensionsError), #[error( - "Extrinsic encoding failed: cannot find a transaction extension version which enough data was given for." + "Extrinsic encoding failed: cannot find a transaction extensions version which relies only on the transaction extensions we were given." )] CannotFindGoodExtensionVersion, } @@ -382,7 +382,7 @@ where .map_err(ExtrinsicEncodeError::CannotEncodeSignature)?; // Signed extensions (now Transaction Extensions) - for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { + for (name, id) in iter_nonempty_extension_values(ext_info, type_resolver) { transaction_extensions .encode_extension_value_to(name, id, type_resolver, &mut encoded_inner) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; @@ -493,14 +493,14 @@ where encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) - for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { + for (name, id) in iter_nonempty_extension_values(ext_info, type_resolver) { transaction_extensions .encode_extension_value_for_signer_payload_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) - for (name, id) in iter_nonempty_extention_implicits(ext_info, type_resolver) { + for (name, id) in iter_nonempty_extension_implicits(ext_info, type_resolver) { transaction_extensions .encode_extension_implicit_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; @@ -897,14 +897,14 @@ where // Encode the "inner" bytes let mut encoded_inner = Vec::new(); - // "is signed" + transaction protocol version (4) + // "is signed" + transaction protocol version (5) (0b01000000 + 5u8).encode_to(&mut encoded_inner); // Version of the transaction extensions. transaction_extension_version.encode_to(&mut encoded_inner); // Transaction Extensions next. These may include a signature/address - for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { + for (name, id) in iter_nonempty_extension_values(ext_info, type_resolver) { transaction_extensions .encode_extension_value_to(name, id, type_resolver, &mut encoded_inner) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; @@ -1025,14 +1025,14 @@ where encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; // Then the signer payload value (ie roughly the bytes that will appear in the tx) - for (name, id) in iter_nonempty_extention_values(ext_info, type_resolver) { + for (name, id) in iter_nonempty_extension_values(ext_info, type_resolver) { transaction_extensions .encode_extension_value_for_signer_payload_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; } // Then the signer payload implicits (ie data we want to verify that is NOT in the tx) - for (name, id) in iter_nonempty_extention_implicits(ext_info, type_resolver) { + for (name, id) in iter_nonempty_extension_implicits(ext_info, type_resolver) { transaction_extensions .encode_extension_implicit_to(name, id, type_resolver, &mut out) .map_err(ExtrinsicEncodeError::TransactionExtensions)?; @@ -1179,7 +1179,7 @@ enum TransactionVersion { } /// Iterate over the non-empty extension implicit name/IDs -fn iter_nonempty_extention_implicits<'exts, 'info, Resolver: TypeResolver>( +fn iter_nonempty_extension_implicits<'exts, 'info, Resolver: TypeResolver>( extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, types: &Resolver, ) -> impl Iterator { @@ -1191,7 +1191,7 @@ fn iter_nonempty_extention_implicits<'exts, 'info, Resolver: TypeResolver>( } /// Iterate over the non-empty extension value name/IDs -fn iter_nonempty_extention_values<'exts, 'info, Resolver: TypeResolver>( +fn iter_nonempty_extension_values<'exts, 'info, Resolver: TypeResolver>( extension_info: &'exts ExtrinsicExtensionInfo<'info, Resolver::TypeId>, types: &Resolver, ) -> impl Iterator { @@ -1203,7 +1203,7 @@ fn iter_nonempty_extention_values<'exts, 'info, Resolver: TypeResolver>( } /// Checks to see whether the type being given is empty, ie would require -/// 0 bytes to encode. We use this to skip 0 byte trasnsaction extensions; ones +/// 0 bytes to encode. We use this to skip 0 byte transaction extensions; ones /// that are mentioned in the metadata but only used in the node side and require /// no bytes to be given. fn is_type_empty(type_id: Resolver::TypeId, types: &Resolver) -> bool { diff --git a/src/methods/extrinsic_encoder/transaction_extensions.rs b/src/methods/extrinsic_encoder/transaction_extensions.rs index d65ea69..1675fc0 100644 --- a/src/methods/extrinsic_encoder/transaction_extensions.rs +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -146,9 +146,7 @@ macro_rules! impl_tuples { // Protection: if we are returning an error then // no bytes should have been encoded to the given // Vec. Ensure that this is true: - while out.len() > len { - out.pop(); - } + out.truncate(len); TransactionExtensionsError::Other { extension_name: name.to_owned(), error: e, @@ -176,9 +174,7 @@ macro_rules! impl_tuples { // Protection: if we are returning an error then // no bytes should have been encoded to the given // Vec. Ensure that this is true: - while out.len() > len { - out.pop(); - } + out.truncate(len); TransactionExtensionsError::Other { extension_name: name.to_owned(), error: e, @@ -206,9 +202,7 @@ macro_rules! impl_tuples { // Protection: if we are returning an error then // no bytes should have been encoded to the given // Vec. Ensure that this is true: - while out.len() > len { - out.pop(); - } + out.truncate(len); TransactionExtensionsError::Other { extension_name: name.to_owned(), error: e, From 9756cf1d6df8c699450fef67fac1fd3f329d60ef Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 13 Feb 2026 10:15:45 +0000 Subject: [PATCH 12/12] Comment tweak --- src/methods/extrinsic_encoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/methods/extrinsic_encoder.rs b/src/methods/extrinsic_encoder.rs index 36978b6..0951454 100644 --- a/src/methods/extrinsic_encoder.rs +++ b/src/methods/extrinsic_encoder.rs @@ -897,7 +897,7 @@ where // Encode the "inner" bytes let mut encoded_inner = Vec::new(); - // "is signed" + transaction protocol version (5) + // "is signed" (2 bits now) + transaction protocol version (5) (0b01000000 + 5u8).encode_to(&mut encoded_inner); // Version of the transaction extensions.