use crate::{
blocks::extrinsic_transaction_extensions::ExtrinsicTransactionExtensions,
config::{Config, HashFor, Hasher},
error::{ExtrinsicDecodeErrorAt, ExtrinsicDecodeErrorAtReason, ExtrinsicError},
Metadata,
};
use alloc::{sync::Arc, vec::Vec};
use frame_decode::extrinsics::Extrinsic;
use scale_decode::{DecodeAsFields, DecodeAsType};
pub use crate::blocks::StaticExtrinsic;
pub struct Extrinsics<T: Config> {
extrinsics: Vec<Arc<(Extrinsic<'static, u32>, Vec<u8>)>>,
metadata: Metadata,
hasher: T::Hasher,
_marker: core::marker::PhantomData<T>,
}
impl<T: Config> Extrinsics<T> {
pub fn decode_from(
extrinsics: Vec<Vec<u8>>,
metadata: Metadata,
) -> Result<Self, ExtrinsicDecodeErrorAt> {
let hasher = T::Hasher::new(&metadata);
let extrinsics = extrinsics
.into_iter()
.enumerate()
.map(|(extrinsic_index, bytes)| {
let cursor = &mut &*bytes;
let decoded_info =
frame_decode::extrinsics::decode_extrinsic(cursor, &metadata, metadata.types())
.map_err(|error| ExtrinsicDecodeErrorAt {
extrinsic_index,
error: ExtrinsicDecodeErrorAtReason::DecodeError(error),
})?
.into_owned();
if !cursor.is_empty() {
return Err(ExtrinsicDecodeErrorAt {
extrinsic_index,
error: ExtrinsicDecodeErrorAtReason::LeftoverBytes(cursor.to_vec()),
});
}
Ok(Arc::new((decoded_info, bytes)))
})
.collect::<Result<_, ExtrinsicDecodeErrorAt>>()?;
Ok(Self { extrinsics, hasher, metadata, _marker: core::marker::PhantomData })
}
pub fn len(&self) -> usize {
self.extrinsics.len()
}
pub fn is_empty(&self) -> bool {
self.extrinsics.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicDetails<T>> + Send + Sync + 'static {
let extrinsics = self.extrinsics.clone();
let num_extrinsics = self.extrinsics.len();
let hasher = self.hasher;
let metadata = self.metadata.clone();
(0..num_extrinsics).map(move |index| {
ExtrinsicDetails::new(index as u32, extrinsics[index].clone(), hasher, metadata.clone())
})
}
pub fn find<E: StaticExtrinsic>(
&self,
) -> impl Iterator<Item = Result<FoundExtrinsic<T, E>, ExtrinsicError>> {
self.iter().filter_map(|details| {
match details.as_extrinsic::<E>() {
Err(err) => Some(Err(err)),
Ok(None) => None,
Ok(Some(value)) => Some(Ok(FoundExtrinsic { details, value })),
}
})
}
pub fn find_first<E: StaticExtrinsic>(
&self,
) -> Result<Option<FoundExtrinsic<T, E>>, ExtrinsicError> {
self.find::<E>().next().transpose()
}
pub fn find_last<E: StaticExtrinsic>(
&self,
) -> Result<Option<FoundExtrinsic<T, E>>, ExtrinsicError> {
self.find::<E>().last().transpose()
}
pub fn has<E: StaticExtrinsic>(&self) -> Result<bool, ExtrinsicError> {
Ok(self.find::<E>().next().transpose()?.is_some())
}
}
pub struct ExtrinsicDetails<T: Config> {
index: u32,
ext: Arc<(Extrinsic<'static, u32>, Vec<u8>)>,
hasher: T::Hasher,
metadata: Metadata,
_marker: core::marker::PhantomData<T>,
}
impl<T> ExtrinsicDetails<T>
where
T: Config,
{
#[doc(hidden)]
pub fn new(
index: u32,
ext: Arc<(Extrinsic<'static, u32>, Vec<u8>)>,
hasher: T::Hasher,
metadata: Metadata,
) -> ExtrinsicDetails<T> {
ExtrinsicDetails { index, ext, hasher, metadata, _marker: core::marker::PhantomData }
}
pub fn hash(&self) -> HashFor<T> {
self.hasher.hash(self.bytes())
}
pub fn is_signed(&self) -> bool {
self.decoded_info().is_signed()
}
pub fn index(&self) -> u32 {
self.index
}
pub fn bytes(&self) -> &[u8] {
&self.ext.1
}
pub fn call_bytes(&self) -> &[u8] {
&self.bytes()[self.decoded_info().call_data_range()]
}
pub fn field_bytes(&self) -> &[u8] {
&self.bytes()[self.decoded_info().call_data_args_range()]
}
pub fn address_bytes(&self) -> Option<&[u8]> {
self.decoded_info()
.signature_payload()
.map(|s| &self.bytes()[s.address_range()])
}
pub fn signature_bytes(&self) -> Option<&[u8]> {
self.decoded_info()
.signature_payload()
.map(|s| &self.bytes()[s.signature_range()])
}
pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> {
self.decoded_info()
.transaction_extension_payload()
.map(|t| &self.bytes()[t.range()])
}
pub fn transaction_extensions(&self) -> Option<ExtrinsicTransactionExtensions<'_, T>> {
self.decoded_info()
.transaction_extension_payload()
.map(|t| ExtrinsicTransactionExtensions::new(self.bytes(), &self.metadata, t))
}
pub fn pallet_index(&self) -> u8 {
self.decoded_info().pallet_index()
}
pub fn call_index(&self) -> u8 {
self.decoded_info().call_index()
}
pub fn pallet_name(&self) -> &str {
self.decoded_info().pallet_name()
}
pub fn call_name(&self) -> &str {
self.decoded_info().call_name()
}
pub fn decode_as_fields<E: DecodeAsFields>(&self) -> Result<E, ExtrinsicError> {
let bytes = &mut self.field_bytes();
let mut fields = self.decoded_info().call_data().map(|d| {
let name = if d.name().is_empty() { None } else { Some(d.name()) };
scale_decode::Field::new(*d.ty(), name)
});
let decoded =
E::decode_as_fields(bytes, &mut fields, self.metadata.types()).map_err(|e| {
ExtrinsicError::CannotDecodeFields {
extrinsic_index: self.index as usize,
error: e,
}
})?;
Ok(decoded)
}
pub fn as_extrinsic<E: StaticExtrinsic>(&self) -> Result<Option<E>, ExtrinsicError> {
if self.decoded_info().pallet_name() == E::PALLET
&& self.decoded_info().call_name() == E::CALL
{
let mut fields = self.decoded_info().call_data().map(|d| {
let name = if d.name().is_empty() { None } else { Some(d.name()) };
scale_decode::Field::new(*d.ty(), name)
});
let decoded =
E::decode_as_fields(&mut self.field_bytes(), &mut fields, self.metadata.types())
.map_err(|e| ExtrinsicError::CannotDecodeFields {
extrinsic_index: self.index as usize,
error: e,
})?;
Ok(Some(decoded))
} else {
Ok(None)
}
}
pub fn as_root_extrinsic<E: DecodeAsType>(&self) -> Result<E, ExtrinsicError> {
let decoded = E::decode_as_type(
&mut &self.call_bytes()[..],
self.metadata.outer_enums().call_enum_ty(),
self.metadata.types(),
)
.map_err(|e| ExtrinsicError::CannotDecodeIntoRootExtrinsic {
extrinsic_index: self.index as usize,
error: e,
})?;
Ok(decoded)
}
fn decoded_info(&self) -> &Extrinsic<'static, u32> {
&self.ext.0
}
}
pub struct FoundExtrinsic<T: Config, E> {
pub details: ExtrinsicDetails<T>,
pub value: E,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::BizinikiwConfig;
use assert_matches::assert_matches;
use codec::{Decode, Encode};
use frame_metadata::{
v15::{
CustomMetadata, ExtrinsicMetadata, OuterEnums, PalletCallMetadata, PalletMetadata,
RuntimeMetadataV15,
},
RuntimeMetadataPrefixed,
};
use scale_info::{meta_type, TypeInfo};
use scale_value::Value;
#[allow(unused)]
#[derive(TypeInfo)]
struct ExtrinsicType<Address, Call, Signature, Extra> {
pub signature: Option<(Address, Signature, Extra)>,
pub function: Call,
}
#[allow(unused)]
#[derive(
Encode,
Decode,
TypeInfo,
Clone,
Debug,
PartialEq,
Eq,
scale_encode::EncodeAsType,
scale_decode::DecodeAsType,
)]
enum RuntimeCall {
Test(Pallet),
}
#[allow(unused)]
#[derive(
Encode,
Decode,
TypeInfo,
Clone,
Debug,
PartialEq,
Eq,
scale_encode::EncodeAsType,
scale_decode::DecodeAsType,
)]
enum Pallet {
#[allow(unused)]
#[codec(index = 2)]
TestCall { value: u128, signed: bool, name: String },
}
#[allow(unused)]
#[derive(
Encode,
Decode,
TypeInfo,
Clone,
Debug,
PartialEq,
Eq,
scale_encode::EncodeAsType,
scale_decode::DecodeAsType,
)]
struct TestCallExtrinsic {
value: u128,
signed: bool,
name: String,
}
impl StaticExtrinsic for TestCallExtrinsic {
const PALLET: &'static str = "Test";
const CALL: &'static str = "TestCall";
}
fn metadata() -> Metadata {
let pallets = vec![PalletMetadata {
name: "Test",
storage: None,
calls: Some(PalletCallMetadata { ty: meta_type::<Pallet>() }),
event: None,
constants: vec![],
error: None,
index: 0,
docs: vec![],
}];
let extrinsic = ExtrinsicMetadata {
version: 4,
signed_extensions: vec![],
address_ty: meta_type::<()>(),
call_ty: meta_type::<RuntimeCall>(),
signature_ty: meta_type::<()>(),
extra_ty: meta_type::<()>(),
};
let meta = RuntimeMetadataV15::new(
pallets,
extrinsic,
meta_type::<()>(),
vec![],
OuterEnums {
call_enum_ty: meta_type::<RuntimeCall>(),
event_enum_ty: meta_type::<()>(),
error_enum_ty: meta_type::<()>(),
},
CustomMetadata { map: Default::default() },
);
let runtime_metadata: RuntimeMetadataPrefixed = meta.into();
let metadata: pezkuwi_subxt_metadata::Metadata = runtime_metadata.try_into().unwrap();
metadata
}
#[test]
fn extrinsic_metadata_consistency() {
let metadata = metadata();
let pallet = metadata.pallet_by_call_index(0).expect("pallet exists");
let extrinsic = pallet
.call_variant_by_index(2)
.expect("metadata contains the RuntimeCall enum with this pallet");
assert_eq!(pallet.name(), "Test");
assert_eq!(&extrinsic.name, "TestCall");
}
#[test]
fn insufficient_extrinsic_bytes() {
let metadata = metadata();
let result = Extrinsics::<BizinikiwConfig>::decode_from(vec![vec![]], metadata);
assert_matches!(
result.err(),
Some(crate::error::ExtrinsicDecodeErrorAt { extrinsic_index: 0, error: _ })
);
}
#[test]
fn unsupported_version_extrinsic() {
use frame_decode::extrinsics::ExtrinsicDecodeError;
let metadata = metadata();
let result = Extrinsics::<BizinikiwConfig>::decode_from(vec![vec![3u8].encode()], metadata);
assert_matches!(
result.err(),
Some(crate::error::ExtrinsicDecodeErrorAt {
extrinsic_index: 0,
error: ExtrinsicDecodeErrorAtReason::DecodeError(
ExtrinsicDecodeError::VersionNotSupported(3)
),
})
);
}
#[test]
fn tx_hashes_line_up() {
let metadata = metadata();
let hasher = <BizinikiwConfig as Config>::Hasher::new(&metadata);
let tx = crate::dynamic::tx(
"Test",
"TestCall",
vec![Value::u128(10), Value::bool(true), Value::string("SomeValue")],
);
let tx_encoded = crate::tx::create_v4_unsigned::<BizinikiwConfig, _>(&tx, &metadata)
.expect("Valid dynamic parameters are provided");
let extrinsics = Extrinsics::<BizinikiwConfig>::decode_from(
vec![tx_encoded.encoded().to_owned()],
metadata,
)
.expect("Valid extrinsic");
let extrinsic = extrinsics.iter().next().unwrap();
assert_eq!(tx_encoded.encoded(), extrinsic.bytes(), "bytes should eq");
assert_eq!(tx_encoded.hash_with(hasher), extrinsic.hash(), "hashes should eq");
}
#[test]
fn statically_decode_extrinsic() {
let metadata = metadata();
let tx = crate::dynamic::tx(
"Test",
"TestCall",
vec![Value::u128(10), Value::bool(true), Value::string("SomeValue")],
);
let tx_encoded = crate::tx::create_v4_unsigned::<BizinikiwConfig, _>(&tx, &metadata)
.expect("Valid dynamic parameters are provided");
let extrinsics = Extrinsics::<BizinikiwConfig>::decode_from(
vec![tx_encoded.encoded().to_owned()],
metadata,
)
.expect("Valid extrinsic");
let extrinsic = extrinsics.iter().next().unwrap();
assert!(!extrinsic.is_signed());
assert_eq!(extrinsic.index(), 0);
assert_eq!(extrinsic.pallet_index(), 0);
assert_eq!(extrinsic.pallet_name(), "Test");
assert_eq!(extrinsic.call_index(), 2);
assert_eq!(extrinsic.call_name(), "TestCall");
let decoded_extrinsic = extrinsic
.as_root_extrinsic::<RuntimeCall>()
.expect("can decode extrinsic to root enum");
assert_eq!(
decoded_extrinsic,
RuntimeCall::Test(Pallet::TestCall {
value: 10,
signed: true,
name: "SomeValue".into(),
})
);
let decoded_extrinsic = extrinsic
.as_extrinsic::<TestCallExtrinsic>()
.expect("can decode extrinsic to extrinsic variant")
.expect("value cannot be None");
assert_eq!(
decoded_extrinsic,
TestCallExtrinsic { value: 10, signed: true, name: "SomeValue".into() }
);
}
}