diff --git a/codegen/src/types/substitutes.rs b/codegen/src/types/substitutes.rs index 683bfe1e9b..ecddaf660d 100644 --- a/codegen/src/types/substitutes.rs +++ b/codegen/src/types/substitutes.rs @@ -99,6 +99,14 @@ impl TypeSubstitutes { parse_quote!(#crate_path::utils::KeyedVec), ), (path_segments!(BTreeSet), parse_quote!(::std::vec::Vec)), + // The `UncheckedExtrinsic(pub Vec)` is part of the runtime API calls. + // The inner bytes represent the encoded extrinsic, however when deriving the + // `EncodeAsType` the bytes would be re-encoded. This leads to the bytes + // being altered by adding the length prefix in front of them. + ( + path_segments!(sp_runtime::generic::unchecked_extrinsic::UncheckedExtrinsic), + parse_quote!(#crate_path::utils::UncheckedExtrinsic), + ), ]; let default_substitutes = defaults @@ -339,7 +347,7 @@ impl From<&scale_info::Path> for PathSegments { /// to = ::subxt::utils::Static<::sp_runtime::MultiAddress> /// ``` /// -/// And we encounter a `sp_runtime::MultiAddress`, then we will pass the `::sp_runtime::MultiAddress` +/// And we encounter a `sp_runtime::MultiAddress`, then we will pass the `::sp_runtime::MultiAddress` /// type param value into this call to turn it into `::sp_runtime::MultiAddress`. fn replace_path_params_recursively, P: Borrow>( path: &mut syn::Path, diff --git a/subxt/src/utils/mod.rs b/subxt/src/utils/mod.rs index f7bd186605..a9965355cc 100644 --- a/subxt/src/utils/mod.rs +++ b/subxt/src/utils/mod.rs @@ -9,6 +9,7 @@ pub mod bits; mod multi_address; mod multi_signature; mod static_type; +mod unchecked_extrinsic; mod wrapper_opaque; use codec::{Compact, Decode, Encode}; @@ -18,6 +19,7 @@ pub use account_id::AccountId32; pub use multi_address::MultiAddress; pub use multi_signature::MultiSignature; pub use static_type::Static; +pub use unchecked_extrinsic::UncheckedExtrinsic; pub use wrapper_opaque::WrapperKeepOpaque; // Used in codegen @@ -26,7 +28,7 @@ pub use primitive_types::{H160, H256, H512}; /// Wraps an already encoded byte vector, prevents being encoded as a raw byte vector as part of /// the transaction payload -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Decode)] pub struct Encoded(pub Vec); impl codec::Encode for Encoded { diff --git a/subxt/src/utils/unchecked_extrinsic.rs b/subxt/src/utils/unchecked_extrinsic.rs new file mode 100644 index 0000000000..882b490bed --- /dev/null +++ b/subxt/src/utils/unchecked_extrinsic.rs @@ -0,0 +1,136 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! The "default" Substrate/Polkadot UncheckedExtrinsic. +//! This is used in codegen for runtime API calls. +//! +//! The inner bytes represent the encoded extrinsic expected by the +//! runtime APIs. Deriving `EncodeAsType` would lead to the inner +//! bytes to be re-encoded (length prefixed). + +use std::marker::PhantomData; + +use codec::{Decode, Encode}; +use scale_decode::{visitor::DecodeAsTypeResult, DecodeAsType, IntoVisitor, Visitor}; + +use super::{Encoded, Static}; + +/// The unchecked extrinsic from substrate. +#[derive(Clone, Debug, Eq, PartialEq, Encode)] +pub struct UncheckedExtrinsic( + Static, + #[codec(skip)] PhantomData<(Address, Call, Signature, Extra)>, +); + +impl UncheckedExtrinsic { + /// Construct a new [`UncheckedExtrinsic`]. + pub fn new(bytes: Vec) -> Self { + Self(Static(Encoded(bytes)), PhantomData) + } + + /// Get the bytes of the encoded extrinsic. + pub fn bytes(&self) -> &[u8] { + self.0 .0 .0.as_slice() + } +} + +impl Decode + for UncheckedExtrinsic +{ + fn decode(input: &mut I) -> Result { + // The bytes for an UncheckedExtrinsic are first a compact + // encoded length, and then the bytes following. This is the + // same encoding as a Vec, so easiest ATM is just to decode + // into that, and then encode the vec bytes to get our extrinsic + // bytes, which we save into an `Encoded` to preserve as-is. + let xt_vec: Vec = Decode::decode(input)?; + Ok(UncheckedExtrinsic::new(xt_vec)) + } +} + +impl scale_encode::EncodeAsType + for UncheckedExtrinsic +{ + fn encode_as_type_to( + &self, + type_id: u32, + types: &scale_info::PortableRegistry, + out: &mut Vec, + ) -> Result<(), scale_encode::Error> { + self.0.encode_as_type_to(type_id, types, out) + } +} + +impl From> + for UncheckedExtrinsic +{ + fn from(bytes: Vec) -> Self { + UncheckedExtrinsic::new(bytes) + } +} + +impl From> + for Vec +{ + fn from(bytes: UncheckedExtrinsic) -> Self { + bytes.0 .0 .0 + } +} + +pub struct UncheckedExtrinsicDecodeAsTypeVisitor( + PhantomData<(Address, Call, Signature, Extra)>, +); + +impl Visitor + for UncheckedExtrinsicDecodeAsTypeVisitor +{ + type Value<'scale, 'info> = UncheckedExtrinsic; + type Error = scale_decode::Error; + + fn unchecked_decode_as_type<'scale, 'info>( + self, + input: &mut &'scale [u8], + type_id: scale_decode::visitor::TypeId, + types: &'info scale_info::PortableRegistry, + ) -> DecodeAsTypeResult, Self::Error>> { + DecodeAsTypeResult::Decoded(Self::Value::decode_as_type(input, type_id.0, types)) + } +} + +impl IntoVisitor + for UncheckedExtrinsic +{ + type Visitor = UncheckedExtrinsicDecodeAsTypeVisitor; + + fn into_visitor() -> Self::Visitor { + UncheckedExtrinsicDecodeAsTypeVisitor(PhantomData) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + fn unchecked_extrinsic_encoding() { + // A tx is basically some bytes with a compact length prefix; ie an encoded vec: + let tx_bytes = vec![1u8, 2, 3].encode(); + + let unchecked_extrinsic = UncheckedExtrinsic::<(), (), (), ()>::new(tx_bytes.clone()); + let encoded_tx_bytes = unchecked_extrinsic.encode(); + + // The encoded representation must not alter the provided bytes. + assert_eq!(tx_bytes, encoded_tx_bytes); + + // However, for decoding we expect to be able to read the extrinsic from the wire + // which would be length prefixed. + let decoded_tx = UncheckedExtrinsic::<(), (), (), ()>::decode(&mut &tx_bytes[..]).unwrap(); + let decoded_tx_bytes = decoded_tx.bytes(); + let encoded_tx_bytes = decoded_tx.encode(); + + assert_eq!(decoded_tx_bytes, encoded_tx_bytes); + // Ensure we can decode the tx and fetch only the tx bytes. + assert_eq!(vec![1, 2, 3], encoded_tx_bytes); + } +} diff --git a/testing/integration-tests/src/full_client/runtime_api/mod.rs b/testing/integration-tests/src/full_client/runtime_api/mod.rs index a87be32fc7..9027f6c00d 100644 --- a/testing/integration-tests/src/full_client/runtime_api/mod.rs +++ b/testing/integration-tests/src/full_client/runtime_api/mod.rs @@ -3,6 +3,7 @@ // see LICENSE for license details. use crate::{node_runtime, test_context}; +use codec::Encode; use subxt::utils::AccountId32; use subxt_signer::sr25519::dev; @@ -47,3 +48,57 @@ async fn account_nonce() -> Result<(), subxt::Error> { Ok(()) } + +#[tokio::test] +async fn unchecked_extrinsic_encoding() -> Result<(), subxt::Error> { + let ctx = test_context().await; + let api = ctx.client(); + + let alice = dev::alice(); + let bob = dev::bob(); + let bob_address = bob.public_key().to_address(); + + // Construct a tx from Alice to Bob. + let tx = node_runtime::tx().balances().transfer(bob_address, 10_000); + + let signed_extrinsic = api + .tx() + .create_signed(&tx, &alice, Default::default()) + .await + .unwrap(); + + let tx_bytes = signed_extrinsic.into_encoded(); + let len = tx_bytes.len() as u32; + + // Manually encode the runtime API call arguments to make a raw call. + let mut encoded = tx_bytes.clone(); + encoded.extend(len.encode()); + + let expected_result: node_runtime::runtime_types::pallet_transaction_payment::types::FeeDetails< + ::core::primitive::u128, + > = api + .runtime_api() + .at_latest() + .await? + .call_raw( + "TransactionPaymentApi_query_fee_details", + Some(encoded.as_ref()), + ) + .await?; + + // Use the generated API to confirm the result with the raw call. + let runtime_api_call = node_runtime::apis() + .transaction_payment_api() + .query_fee_details(tx_bytes.into(), len); + + let result = api + .runtime_api() + .at_latest() + .await? + .call(runtime_api_call) + .await?; + + assert_eq!(expected_result, result); + + Ok(()) +}