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/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/lib.rs b/src/lib.rs index 04ae75c..71b1d3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,9 +39,21 @@ pub mod extrinsics { Extrinsic, ExtrinsicDecodeError, ExtrinsicExtensions, ExtrinsicOwned, ExtrinsicSignature, ExtrinsicType, NamedArg, decode_extrinsic, }; + pub use crate::methods::extrinsic_encoder::{ + ExtrinsicEncodeError, TransactionExtension, TransactionExtensionError, + TransactionExtensions, TransactionExtensionsError, + 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, + 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, - ExtrinsicSignatureInfo, ExtrinsicTypeInfo, + ExtrinsicCallInfo, ExtrinsicCallInfoArg, ExtrinsicExtensionInfo, ExtrinsicExtensionInfoArg, + ExtrinsicInfoError, ExtrinsicSignatureInfo, ExtrinsicTypeInfo, }; } 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..0951454 --- /dev/null +++ b/src/methods/extrinsic_encoder.rs @@ -0,0 +1,1245 @@ +// 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_extension; +mod transaction_extensions; +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}; + +/// 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 extensions version which relies only on the transaction extensions we were given." + )] + CannotFindGoodExtensionVersion, +} + +/// 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, + 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) +} + +/// 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, + 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) +} + +/// 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, + 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, + ) +} + +/// 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(); +/// ``` +#[allow(clippy::too_many_arguments)] +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) +} + +/// 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(); +/// ``` +#[allow(clippy::too_many_arguments)] +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, + ) +} + +/// 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. +#[allow(clippy::too_many_arguments)] +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 (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)?; + } + + // 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)?; + + // Now, encoding these inner bytes prefixes the compact length to the beginning: + encoded_inner.encode_to(out); + 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, + 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, + ) +} + +/// 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> +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)?; + + // Then the signer payload value (ie roughly the bytes that will appear in the tx) + 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_extension_implicits(ext_info, type_resolver) { + transaction_extensions + .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(); + } + + Ok(out) +} + +/// 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> +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) +} + +/// 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, + 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) +} + +/// 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, + 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, + ) +} + +/// 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, + type_resolver: &Resolver, +) -> Result +where + Info: ExtrinsicTypeInfo, + Exts: TransactionExtensions, + Resolver: TypeResolver, + Info::TypeId: Clone, +{ + 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| { + 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. + if have_data { + return Ok(ext_version); + } + } + + 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, + 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) +} + +/// 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(); +/// ``` +#[allow(clippy::too_many_arguments)] +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)?; + + encode_v5_general_with_info_to( + call_data, + transaction_extension_version, + transaction_extensions, + type_resolver, + &call_info, + &ext_info, + out, + ) +} + +/// 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, + 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" (2 bits now) + 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_extension_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)?; + + // Now, encoding these inner bytes prefixes the compact length to the beginning: + encoded_inner.encode_to(out); + 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, + 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, + ) +} + +/// 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> +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)?; + + // Then the signer payload value (ie roughly the bytes that will appear in the tx) + 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_extension_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). + 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()`], but +/// with a byte for the pallet index and call index prepended. +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()`], 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, + 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()`], 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, +) -> Result<(), ExtrinsicEncodeError> +where + Resolver: TypeResolver, + CallData: EncodeAsFields, +{ + // Pallet and call index to identify the call: + 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 { + 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( + 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); + // Then the arguments for the call: + encode_call_data_with_info_to(call_data, call_info, type_resolver, &mut out)?; + 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, +} + +/// Iterate over the non-empty extension implicit name/IDs +fn iter_nonempty_extension_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_extension_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 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 { + 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_extension.rs b/src/methods/extrinsic_encoder/transaction_extension.rs new file mode 100644 index 0000000..ac5e266 --- /dev/null +++ b/src/methods/extrinsic_encoder/transaction_extension.rs @@ -0,0 +1,67 @@ +// 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 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. +/// 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. + const NAME: &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..1675fc0 --- /dev/null +++ b/src/methods/extrinsic_encoder/transaction_extensions.rs @@ -0,0 +1,248 @@ +// 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 super::transaction_extension::{TransactionExtension, TransactionExtensionError}; +use alloc::borrow::ToOwned; +use alloc::string::String; +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`]. +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 { + /// 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, + }, +} + +// 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 $ident::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 $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 + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + out.truncate(len); + 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 $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 + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + out.truncate(len); + 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 $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 + // no bytes should have been encoded to the given + // Vec. Ensure that this is true: + out.truncate(len); + 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, 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 b24f869..e57040d 100644 --- a/src/methods/extrinsic_type_info.rs +++ b/src/methods/extrinsic_type_info.rs @@ -18,29 +18,52 @@ use alloc::string::String; use alloc::vec::Vec; use core::write; -#[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. @@ -51,11 +74,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>, @@ -92,6 +123,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, @@ -102,6 +136,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}") @@ -161,6 +205,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, @@ -170,6 +219,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 { @@ -216,22 +274,37 @@ 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. +/// 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. 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. @@ -247,10 +320,90 @@ 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_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())); + ExtrinsicCallInfoArg { 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 @@ -314,11 +467,13 @@ macro_rules! impl_call_arg_ids_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(); Ok(ExtrinsicCallInfo { + pallet_index: $pallet_index, + call_index: $call_index, pallet_name: Cow::Borrowed(pallet_name), call_name: Cow::Borrowed(&call_variant.name), args, @@ -328,12 +483,19 @@ 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<'_>> { - impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) + 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, @@ -355,24 +517,37 @@ 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(); Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } 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<'_>> { - impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) + 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, @@ -392,24 +567,37 @@ 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(); Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } 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<'_>> { - impl_call_arg_ids_body_for_v14_to_v16!(self, pallet_index, call_index) + 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, @@ -446,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), } }) @@ -455,6 +644,15 @@ 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( @@ -527,7 +725,11 @@ 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 alloc::format; + 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); @@ -572,7 +774,82 @@ 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), + }) + }) + .collect::>()?; + + 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(ExtrinsicCallInfoArg { id, name: Cow::Borrowed(name), }) @@ -580,6 +857,8 @@ const _: () = { .collect::>()?; Ok(ExtrinsicCallInfo { + pallet_index, + call_index: call_index as u8, pallet_name: Cow::Borrowed(m_name), call_name: Cow::Borrowed(c_name), args, @@ -591,12 +870,19 @@ 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<'_>> { - impl_extrinsic_info_body_for_v8_to_v11!(self, pallet_index, call_index) + 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, @@ -613,12 +899,20 @@ 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", + )?, }]), }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } }; } @@ -629,12 +923,19 @@ 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<'_>> { - impl_extrinsic_info_body_for_v8_to_v11!(self, pallet_index, call_index) + 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, @@ -659,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), }) }) @@ -669,13 +974,18 @@ const _: () = { Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } macro_rules! impl_for_v12_to_v13 { ($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, @@ -722,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), }) @@ -730,6 +1040,82 @@ const _: () = { .collect::>()?; 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(ExtrinsicCallInfoArg { + 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, @@ -758,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), }) }) @@ -768,6 +1158,11 @@ const _: () = { Ok(ExtrinsicExtensionInfo { extension_ids }) } + fn extrinsic_extension_version_info( + &self, + ) -> Result, ExtrinsicInfoError<'_>> { + Ok(core::iter::empty()) + } } }; } 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; 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;