use crate::{
blocks::block_types::{get_events, CachedEvents},
client::{OfflineClientT, OnlineClientT},
config::{Config, Hasher},
error::{BlockError, Error, MetadataError},
events,
metadata::types::PalletMetadata,
rpc::types::ChainBlockExtrinsic,
Metadata,
};
use codec::Decode;
use derivative::Derivative;
use scale_decode::DecodeAsFields;
use std::{collections::HashMap, sync::Arc};
pub trait StaticExtrinsic: DecodeAsFields {
const PALLET: &'static str;
const CALL: &'static str;
fn is_extrinsic(pallet: &str, call: &str) -> bool {
Self::PALLET == pallet && Self::CALL == call
}
}
#[doc(hidden)]
pub trait RootExtrinsic: Sized {
fn root_extrinsic(
pallet_bytes: &[u8],
pallet_name: &str,
pallet_extrinsic_ty: u32,
metadata: &Metadata,
) -> Result<Self, Error>;
}
pub struct Extrinsics<T: Config, C> {
client: C,
extrinsics: Vec<ChainBlockExtrinsic>,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
hash: T::Hash,
}
impl<T, C> Extrinsics<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
pub(crate) fn new(
client: C,
extrinsics: Vec<ChainBlockExtrinsic>,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
hash: T::Hash,
) -> Self {
Self {
client,
extrinsics,
cached_events,
ids,
hash,
}
}
pub fn len(&self) -> usize {
self.extrinsics.len()
}
pub fn is_empty(&self) -> bool {
self.extrinsics.is_empty()
}
pub fn block_hash(&self) -> T::Hash {
self.hash
}
pub fn iter(
&self,
) -> impl Iterator<Item = Result<ExtrinsicDetails<T, C>, Error>> + Send + Sync + 'static {
let extrinsics = self.extrinsics.clone();
let num_extrinsics = self.extrinsics.len();
let client = self.client.clone();
let hash = self.hash;
let cached_events = self.cached_events.clone();
let ids = self.ids;
let mut index = 0;
std::iter::from_fn(move || {
if index == num_extrinsics {
None
} else {
match ExtrinsicDetails::decode_from(
index as u32,
extrinsics[index].0.clone().into(),
client.clone(),
hash,
cached_events.clone(),
ids,
) {
Ok(extrinsic_details) => {
index += 1;
Some(Ok(extrinsic_details))
}
Err(e) => {
index = num_extrinsics;
Some(Err(e))
}
}
}
})
}
pub fn find<E: StaticExtrinsic>(&self) -> impl Iterator<Item = Result<E, Error>> + '_ {
self.iter().filter_map(|e| {
e.and_then(|e| e.as_extrinsic::<E>().map_err(Into::into))
.transpose()
})
}
pub fn find_first<E: StaticExtrinsic>(&self) -> Result<Option<E>, Error> {
self.find::<E>().next().transpose()
}
pub fn find_last<E: StaticExtrinsic>(&self) -> Result<Option<E>, Error> {
self.find::<E>().last().transpose()
}
pub fn has<E: StaticExtrinsic>(&self) -> Result<bool, Error> {
Ok(self.find::<E>().next().transpose()?.is_some())
}
}
pub struct ExtrinsicDetails<T: Config, C> {
index: u32,
bytes: Arc<[u8]>,
is_signed: bool,
address_start_idx: usize,
address_end_idx: usize,
call_start_idx: usize,
pallet_index: u8,
variant_index: u8,
block_hash: T::Hash,
client: C,
cached_events: CachedEvents<T>,
metadata: Metadata,
_marker: std::marker::PhantomData<T>,
}
impl<T, C> ExtrinsicDetails<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
pub(crate) fn decode_from(
index: u32,
extrinsic_bytes: Arc<[u8]>,
client: C,
block_hash: T::Hash,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
) -> Result<ExtrinsicDetails<T, C>, Error> {
const SIGNATURE_MASK: u8 = 0b1000_0000;
const VERSION_MASK: u8 = 0b0111_1111;
const LATEST_EXTRINSIC_VERSION: u8 = 4;
let metadata = client.metadata();
let first_byte: u8 = Decode::decode(&mut &extrinsic_bytes[..])?;
let version = first_byte & VERSION_MASK;
if version != LATEST_EXTRINSIC_VERSION {
return Err(BlockError::UnsupportedVersion(version).into());
}
let is_signed = first_byte & SIGNATURE_MASK != 0;
let cursor = &mut &extrinsic_bytes[1..];
let mut address_start_idx = 0;
let mut address_end_idx = 0;
if is_signed {
address_start_idx = extrinsic_bytes.len() - cursor.len();
scale_decode::visitor::decode_with_visitor(
cursor,
ids.address,
metadata.types(),
scale_decode::visitor::IgnoreVisitor,
)
.map_err(scale_decode::Error::from)?;
address_end_idx = extrinsic_bytes.len() - cursor.len();
scale_decode::visitor::decode_with_visitor(
cursor,
ids.signature,
metadata.types(),
scale_decode::visitor::IgnoreVisitor,
)
.map_err(scale_decode::Error::from)?;
scale_decode::visitor::decode_with_visitor(
cursor,
ids.extra,
metadata.types(),
scale_decode::visitor::IgnoreVisitor,
)
.map_err(scale_decode::Error::from)?;
}
let call_start_idx = extrinsic_bytes.len() - cursor.len();
let cursor = &mut &extrinsic_bytes[call_start_idx..];
let pallet_index: u8 = Decode::decode(cursor)?;
let variant_index: u8 = Decode::decode(cursor)?;
Ok(ExtrinsicDetails {
index,
bytes: extrinsic_bytes,
is_signed,
address_start_idx,
address_end_idx,
call_start_idx,
pallet_index,
variant_index,
block_hash,
client,
cached_events,
metadata,
_marker: std::marker::PhantomData,
})
}
pub fn is_signed(&self) -> bool {
self.is_signed
}
pub fn index(&self) -> u32 {
self.index
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn call_bytes(&self) -> &[u8] {
&self.bytes[self.call_start_idx..]
}
pub fn field_bytes(&self) -> &[u8] {
&self.call_bytes()[2..]
}
pub fn address_bytes(&self) -> Option<&[u8]> {
self.is_signed
.then(|| &self.bytes[self.address_start_idx..self.address_end_idx])
}
pub fn pallet_index(&self) -> u8 {
self.pallet_index
}
pub fn variant_index(&self) -> u8 {
self.variant_index
}
pub fn pallet_name(&self) -> Result<&str, Error> {
Ok(self.extrinsic_metadata()?.pallet.name())
}
pub fn variant_name(&self) -> Result<&str, Error> {
Ok(&self.extrinsic_metadata()?.variant.name)
}
pub fn extrinsic_metadata(&self) -> Result<ExtrinsicMetadataDetails, Error> {
let pallet = self.metadata.pallet_by_index_err(self.pallet_index())?;
let variant = pallet
.call_variant_by_index(self.variant_index())
.ok_or_else(|| MetadataError::VariantIndexNotFound(self.variant_index()))?;
Ok(ExtrinsicMetadataDetails { pallet, variant })
}
pub fn field_values(
&self,
) -> Result<scale_value::Composite<scale_value::scale::TypeId>, Error> {
let bytes = &mut self.field_bytes();
let extrinsic_metadata = self.extrinsic_metadata()?;
let mut fields = extrinsic_metadata
.variant
.fields
.iter()
.map(|f| scale_decode::Field::new(f.ty.id, f.name.as_deref()));
let decoded = <scale_value::Composite<scale_value::scale::TypeId>>::decode_as_fields(
bytes,
&mut fields,
self.metadata.types(),
)?;
Ok(decoded)
}
pub fn as_extrinsic<E: StaticExtrinsic>(&self) -> Result<Option<E>, Error> {
let extrinsic_metadata = self.extrinsic_metadata()?;
if extrinsic_metadata.pallet.name() == E::PALLET
&& extrinsic_metadata.variant.name == E::CALL
{
let mut fields = extrinsic_metadata
.variant
.fields
.iter()
.map(|f| scale_decode::Field::new(f.ty.id, f.name.as_deref()));
let decoded =
E::decode_as_fields(&mut self.field_bytes(), &mut fields, self.metadata.types())?;
Ok(Some(decoded))
} else {
Ok(None)
}
}
pub fn as_root_extrinsic<E: RootExtrinsic>(&self) -> Result<E, Error> {
let md = self.extrinsic_metadata()?;
let pallet_extrinsic_ty = md.pallet.call_ty_id().ok_or_else(|| {
Error::Metadata(MetadataError::CallTypeNotFoundInPallet(md.pallet.index()))
})?;
E::root_extrinsic(
&self.call_bytes()[1..],
md.pallet.name(),
pallet_extrinsic_ty,
&self.metadata,
)
}
}
impl<T, C> ExtrinsicDetails<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
pub async fn events(&self) -> Result<ExtrinsicEvents<T>, Error> {
let events = get_events(&self.client, self.block_hash, &self.cached_events).await?;
let ext_hash = T::Hasher::hash_of(&self.bytes);
Ok(ExtrinsicEvents::new(ext_hash, self.index, events))
}
}
pub struct ExtrinsicMetadataDetails<'a> {
pub pallet: PalletMetadata<'a>,
pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct ExtrinsicPartTypeIds {
address: u32,
_call: u32,
signature: u32,
extra: u32,
}
impl ExtrinsicPartTypeIds {
pub(crate) fn new(metadata: &Metadata) -> Result<Self, BlockError> {
const ADDRESS: &str = "Address";
const CALL: &str = "Call";
const SIGNATURE: &str = "Signature";
const EXTRA: &str = "Extra";
let id = metadata.extrinsic().ty();
let Some(ty) = metadata.types().resolve(id) else {
return Err(BlockError::MissingType);
};
let params: HashMap<_, _> = ty
.type_params
.iter()
.map(|ty_param| {
let Some(ty) = ty_param.ty else {
return Err(BlockError::MissingType);
};
Ok((ty_param.name.as_str(), ty.id))
})
.collect::<Result<_, _>>()?;
let Some(address) = params.get(ADDRESS) else {
return Err(BlockError::MissingType);
};
let Some(call) = params.get(CALL) else {
return Err(BlockError::MissingType);
};
let Some(signature) = params.get(SIGNATURE) else {
return Err(BlockError::MissingType);
};
let Some(extra) = params.get(EXTRA) else {
return Err(BlockError::MissingType);
};
Ok(ExtrinsicPartTypeIds {
address: *address,
_call: *call,
signature: *signature,
extra: *extra,
})
}
}
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct ExtrinsicEvents<T: Config> {
ext_hash: T::Hash,
idx: u32,
events: events::Events<T>,
}
impl<T: Config> ExtrinsicEvents<T> {
pub(crate) fn new(ext_hash: T::Hash, idx: u32, events: events::Events<T>) -> Self {
Self {
ext_hash,
idx,
events,
}
}
pub fn block_hash(&self) -> T::Hash {
self.events.block_hash()
}
pub fn extrinsic_index(&self) -> u32 {
self.idx
}
pub fn extrinsic_hash(&self) -> T::Hash {
self.ext_hash
}
pub fn all_events_in_block(&self) -> &events::Events<T> {
&self.events
}
pub fn iter(&self) -> impl Iterator<Item = Result<events::EventDetails<T>, Error>> + '_ {
self.events.iter().filter(|ev| {
ev.as_ref()
.map(|ev| ev.phase() == events::Phase::ApplyExtrinsic(self.idx))
.unwrap_or(true) })
}
pub fn find<Ev: events::StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, Error>> + '_ {
self.iter().filter_map(|ev| {
ev.and_then(|ev| ev.as_event::<Ev>().map_err(Into::into))
.transpose()
})
}
pub fn find_first<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, Error> {
self.find::<Ev>().next().transpose()
}
pub fn find_last<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, Error> {
self.find::<Ev>().last().transpose()
}
pub fn has<Ev: events::StaticEvent>(&self) -> Result<bool, Error> {
Ok(self.find::<Ev>().next().transpose()?.is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::DecodeWithMetadata;
use crate::{rpc::types::RuntimeVersion, OfflineClient, PolkadotConfig};
use assert_matches::assert_matches;
use codec::{Decode, Encode};
use frame_metadata::{
v15::{ExtrinsicMetadata, PalletCallMetadata, PalletMetadata, RuntimeMetadataV15},
RuntimeMetadataPrefixed,
};
use primitive_types::H256;
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),
}
impl RootExtrinsic for RuntimeCall {
fn root_extrinsic(
mut pallet_bytes: &[u8],
pallet_name: &str,
pallet_extrinsic_ty: u32,
metadata: &Metadata,
) -> Result<Self, Error> {
if pallet_name == "Test" {
return Ok(RuntimeCall::Test(Pallet::decode_with_metadata(
&mut pallet_bytes,
pallet_extrinsic_ty,
metadata,
)?));
}
panic!(
"Asked for pallet name '{pallet_name}', which isn't in our test RuntimeCall type"
)
}
}
#[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 {
ty: meta_type::<ExtrinsicType<(), RuntimeCall, (), ()>>(),
version: 4,
signed_extensions: vec![],
};
let meta = RuntimeMetadataV15::new(pallets, extrinsic, meta_type::<()>(), vec![]);
let runtime_metadata: RuntimeMetadataPrefixed = meta.into();
Metadata::new(runtime_metadata.try_into().unwrap())
}
fn client(metadata: Metadata) -> OfflineClient<PolkadotConfig> {
let rt_version = RuntimeVersion {
spec_version: 1,
transaction_version: 4,
other: Default::default(),
};
let block_hash = H256::random();
OfflineClient::new(block_hash, rt_version, metadata)
}
#[test]
fn extrinsic_metadata_consistency() {
let metadata = metadata();
let pallet = metadata.pallet_by_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 client = client(metadata.clone());
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
let result = ExtrinsicDetails::decode_from(
1,
vec![].into(),
client,
H256::random(),
Default::default(),
ids,
);
assert_matches!(result.err(), Some(crate::Error::Codec(_)));
}
#[test]
fn unsupported_version_extrinsic() {
let metadata = metadata();
let client = client(metadata.clone());
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
let result = ExtrinsicDetails::decode_from(
1,
3u8.encode().into(),
client,
H256::random(),
Default::default(),
ids,
);
assert_matches!(
result.err(),
Some(crate::Error::Block(
crate::error::BlockError::UnsupportedVersion(3)
))
);
}
#[test]
fn statically_decode_extrinsic() {
let metadata = metadata();
let client = client(metadata.clone());
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
let tx = crate::tx::dynamic(
"Test",
"TestCall",
vec![
Value::u128(10),
Value::bool(true),
Value::string("SomeValue"),
],
);
let tx_encoded = client
.tx()
.create_unsigned(&tx)
.expect("Valid dynamic parameters are provided");
let extrinsic = ExtrinsicDetails::decode_from(
1,
tx_encoded.encoded()[1..].into(),
client,
H256::random(),
Default::default(),
ids,
)
.expect("Valid extrinsic");
assert!(!extrinsic.is_signed());
assert_eq!(extrinsic.index(), 1);
assert_eq!(extrinsic.pallet_index(), 0);
assert_eq!(
extrinsic
.pallet_name()
.expect("Valid metadata contains pallet name"),
"Test"
);
assert_eq!(extrinsic.variant_index(), 2);
assert_eq!(
extrinsic
.variant_name()
.expect("Valid metadata contains variant 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(),
}
);
}
}