Repository: scs/substrate-api-client (language: rust)

Issue:
Add Encode / Decode to ExtrinsicReport
With PR https://github.com/scs/substrate-api-client/pull/386 a new struct `ExtrinsicReport` has been introduced. For a good user experience, it should implement at least Encode and Decode, maybe even Serialize and Deserialize. 

However, because `ExtrinsicReport` contains `EventDetails`, which do not yet support Encode/Decode, it first needs to be investigated how to fix this. Or if it's not fixable, maybe skip the EventDetails during Serilaization with serde::skip or something alike.

See https://github.com/scs/substrate-api-client/pull/386#discussion_r1055177318


Discussion / hints:

Fix patch:
diff --git a/examples/async/examples/check_extrinsic_events.rs b/examples/async/examples/check_extrinsic_events.rs
index ecdd55dc7..f5038d70f 100644
--- a/examples/async/examples/check_extrinsic_events.rs
+++ b/examples/async/examples/check_extrinsic_events.rs
@@ -15,7 +15,7 @@
 
 use sp_keyring::AccountKeyring;
 use substrate_api_client::{
-	ac_node_api::EventDetails,
+	ac_node_api::RawEventDetails,
 	ac_primitives::{AssetRuntimeConfig, Config},
 	extrinsic::BalancesExtrinsics,
 	rpc::JsonrpseeClient,
@@ -122,7 +122,7 @@ async fn main() {
 	assert_eq!(expected_balance_of_bob, new_balance_of_bob);
 }
 
-fn assert_associated_events_match_expected(events: Vec<EventDetails<Hash>>) {
+fn assert_associated_events_match_expected(events: Vec<RawEventDetails<Hash>>) {
 	// First event
 	assert_eq!(events[0].pallet_name(), "Balances");
 	assert_eq!(events[0].variant_name(), "Withdraw");
diff --git a/node-api/src/error/dispatch_error.rs b/node-api/src/error/dispatch_error.rs
index 101757dfc..249d300f5 100644
--- a/node-api/src/error/dispatch_error.rs
+++ b/node-api/src/error/dispatch_error.rs
@@ -59,7 +59,7 @@ impl DispatchError {
 	// https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/error/dispatch_error.rs#L208-L321
 	pub fn decode_from<'a>(
 		bytes: impl Into<Cow<'a, [u8]>>,
-		metadata: Metadata,
+		metadata: &Metadata,
 	) -> Result<Self, Error> {
 		let bytes = bytes.into();
 		let dispatch_error_ty_id =
diff --git a/node-api/src/events/event_details.rs b/node-api/src/events/event_details.rs
index 31d625c59..53a3deb50 100644
--- a/node-api/src/events/event_details.rs
+++ b/node-api/src/events/event_details.rs
@@ -10,41 +10,23 @@
 
 use crate::{
 	error::{DispatchError, Error},
-	metadata::{MetadataError, PalletMetadata},
+	events::{EventMetadataDetails, RawEventDetails, RootEvent},
 	Metadata, Phase, StaticEvent,
 };
-use alloc::{sync::Arc, vec::Vec};
-use codec::Decode;
-use log::*;
-use scale_decode::DecodeAsFields;
+use alloc::sync::Arc;
+use codec::{Decode, Encode};
 use scale_value::{scale::TypeId, Composite};
 
-/// The event details.
-/// Based on subxt EventDetails.
-/// https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L197-L216
-#[derive(Debug, Clone)]
-pub struct EventDetails<Hash: Decode> {
-	phase: Phase,
-	/// The index of the event in the list of events in a given block.
-	index: u32,
-	all_bytes: Arc<[u8]>,
-	// start of the bytes (phase, pallet/variant index and then fields and then topic to follow).
-	start_idx: usize,
-	// start of the event (ie pallet/variant index and then the fields and topic after).
-	event_start_idx: usize,
-	// start of the fields (ie after phase and pallet/variant index).
-	event_fields_start_idx: usize,
-	// end of the fields.
-	event_fields_end_idx: usize,
-	// end of everything (fields + topics)
-	end_idx: usize,
+/// The event details with the associated metadata.
+// Based on subxt EventDetails.
+// https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L197-L216
+#[derive(Debug, Clone, Encode, Decode)]
+pub struct EventDetails<Hash: Encode + Decode> {
+	inner: RawEventDetails<Hash>,
 	metadata: Metadata,
-	topics: Vec<Hash>,
 }
 
-// Based on subxt:
-// https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L218-L409
-impl<Hash: Decode> EventDetails<Hash> {
+impl<Hash: Encode + Decode> EventDetails<Hash> {
 	// Attempt to dynamically decode a single event from our events input.
 	pub(crate) fn decode_from(
 		metadata: Metadata,
@@ -52,104 +34,47 @@ impl<Hash: Decode> EventDetails<Hash> {
 		start_idx: usize,
 		index: u32,
 	) -> Result<Self, Error> {
-		let input = &mut &all_bytes[start_idx..];
-
-		let phase = Phase::decode(input)?;
-
-		let event_start_idx = all_bytes.len() - input.len();
-
-		let pallet_index = u8::decode(input)?;
-		let variant_index = u8::decode(input)?;
-
-		let event_fields_start_idx = all_bytes.len() - input.len();
-
-		// Get metadata for the event:
-		let event_pallet = metadata.pallet_by_index_err(pallet_index)?;
-		let event_variant = event_pallet
-			.event_variant_by_index(variant_index)
-			.ok_or(MetadataError::VariantIndexNotFound(variant_index))?;
-		debug!("Decoding Event '{}::{}'", event_pallet.name(), &event_variant.name);
-
-		// Skip over the bytes belonging to this event.
-		for field_metadata in &event_variant.fields {
-			// Skip over the bytes for this field:
-			scale_decode::visitor::decode_with_visitor(
-				input,
-				field_metadata.ty.id,
-				metadata.types(),
-				scale_decode::visitor::IgnoreVisitor,
-			)
-			.map_err(scale_decode::Error::from)?;
-		}
-
-		// the end of the field bytes.
-		let event_fields_end_idx = all_bytes.len() - input.len();
-
-		// topics come after the event data in EventRecord.
-		let topics = Vec::<Hash>::decode(input)?;
-
-		// what bytes did we skip over in total, including topics.
-		let end_idx = all_bytes.len() - input.len();
-
-		Ok(EventDetails {
-			phase,
-			index,
-			start_idx,
-			event_start_idx,
-			event_fields_start_idx,
-			event_fields_end_idx,
-			end_idx,
-			all_bytes,
-			metadata,
-			topics,
-		})
+		let inner = RawEventDetails::decode_from(&metadata, all_bytes, start_idx, index)?;
+		Ok(EventDetails { inner, metadata })
 	}
 
 	/// When was the event produced?
 	pub fn phase(&self) -> Phase {
-		self.phase
+		self.inner.phase()
 	}
 
 	/// What index is this event in the stored events for this block.
 	pub fn index(&self) -> u32 {
-		self.index
+		self.inner.index()
 	}
 
 	/// The index of the pallet that the event originated from.
 	pub fn pallet_index(&self) -> u8 {
 		// Note: never panics; we expect these bytes to exist
 		// in order that the EventDetails could be created.
-		self.all_bytes[self.event_fields_start_idx - 2]
+		self.inner.pallet_index()
 	}
 
 	/// The index of the event variant that the event originated from.
 	pub fn variant_index(&self) -> u8 {
 		// Note: never panics; we expect these bytes to exist
 		// in order that the EventDetails could be created.
-		self.all_bytes[self.event_fields_start_idx - 1]
+		self.inner.variant_index()
 	}
 
 	/// The name of the pallet from whence the Event originated.
 	pub fn pallet_name(&self) -> &str {
-		self.event_metadata().pallet.name()
+		self.inner.pallet_name()
 	}
 
 	/// The name of the event (ie the name of the variant that it corresponds to).
 	pub fn variant_name(&self) -> &str {
-		&self.event_metadata().variant.name
+		self.inner.variant_name()
 	}
 
 	/// Fetch details from the metadata for this event.
 	pub fn event_metadata(&self) -> EventMetadataDetails {
-		let pallet = self
-			.metadata
-			.pallet_by_index(self.pallet_index())
-			.expect("event pallet to be found; we did this already during decoding");
-		let variant = pallet
-			.event_variant_by_index(self.variant_index())
-			.expect("event variant to be found; we did this already during decoding");
-
-		EventMetadataDetails { pallet, variant }
+		self.inner.event_metadata_unchecked(&self.metadata)
 	}
 
 	/// Return _all_ of the bytes representing this event, which include, in order:
@@ -158,30 +83,18 @@ impl<Hash: Decode> EventDetails<Hash> {
 	/// - Event fields.
 	/// - Event Topics.
 	pub fn bytes(&self) -> &[u8] {
-		&self.all_bytes[self.start_idx..self.end_idx]
+		self.inner.bytes()
 	}
 
 	/// Return the bytes representing the fields stored in this event.
 	pub fn field_bytes(&self) -> &[u8] {
-		&self.all_bytes[self.event_fields_start_idx..self.event_fields_end_idx]
+		self.inner.field_bytes()
 	}
 
 	/// Decode and provide the event fields back in the form of a [`scale_value::Composite`]
 	/// type which represents the named or unnamed fields that were present in the event.
 	pub fn field_values(&self) -> Result<Composite<TypeId>, Error> {
-		let bytes = &mut self.field_bytes();
-		let event_metadata = self.event_metadata();
-
-		let mut fields = event_metadata
-			.variant
-			.fields
-			.iter()
-			.map(|f| scale_decode::Field::new(f.ty.id, f.name.as_deref()));
-
-		let decoded =
-			<Composite<TypeId>>::decode_as_fields(bytes, &mut fields, self.metadata.types())?;
-
-		Ok(decoded)
+		self.inner.field_values_unchecked(&self.metadata)
 	}
 
 	/// Attempt to decode these [`EventDetails`] into a specific static event.
@@ -189,73 +102,41 @@ impl<Hash: Decode> EventDetails<Hash> {
 	/// decode the entirety of the event type (including the pallet and event
 	/// variants) using [`EventDetails::as_root_event()`].
 	pub fn as_event<E: StaticEvent>(&self) -> Result<Option<E>, Error> {
-		let ev_metadata = self.event_metadata();
-		if ev_metadata.pallet.name() == E::PALLET && ev_metadata.variant.name == E::EVENT {
-			Ok(Some(E::decode(&mut self.field_bytes())?))
-		} else {
-			Ok(None)
-		}
+		self.inner.as_event()
 	}
 
 	/// Attempt to decode these [`EventDetails`] into a root event type (which includes
 	/// the pallet and event enum variants as well as the event fields). A compatible
 	/// type for this is exposed via static codegen as a root level `Event` type.
 	pub fn as_root_event<E: RootEvent>(&self) -> Result<E, Error> {
-		let ev_metadata = self.event_metadata();
-		let pallet_bytes = &self.all_bytes[self.event_start_idx + 1..self.event_fields_end_idx];
-		let pallet_event_ty = ev_metadata
-			.pallet
-			.event_ty_id()
-			.ok_or_else(|| MetadataError::EventTypeNotFoundInPallet(ev_metadata.pallet.index()))?;
-
-		E::root_event(pallet_bytes, self.pallet_name(), pallet_event_ty, &self.metadata)
+		self.inner.as_root_event_unchecked(&self.metadata)
 	}
 
 	/// Return the topics associated with this event.
 	pub fn topics(&self) -> &[Hash] {
-		&self.topics
+		self.inner.topics()
+	}
+
+	/// Consume original struct and return only the raw portion without metadata.
+	pub fn to_raw(self) -> RawEventDetails<Hash> {
+		self.inner
 	}
 }
 
-impl<Hash: Decode> EventDetails<Hash> {
-	/// Checks if the extrinsic has failed. If so, the corresponding DispatchError is returned.
-	pub fn check_if_failed(&self) -> Result<(), DispatchError> {
-		if self.pallet_name() == "System" && self.variant_name() == "ExtrinsicFailed" {
-			let dispatch_error =
-				DispatchError::decode_from(self.field_bytes(), self.metadata.clone())
-					.map_err(|_| DispatchError::CannotLookup)?;
-			return Err(dispatch_error)
-		}
-		Ok(())
+impl<Hash: Encode + Decode> EventDetails<Hash> {
+	/// Checks if the extrinsic has failed.
+	pub fn has_failed(&self) -> bool {
+		self.inner.has_failed()
+	}
+
+	/// Returns the dispatch error of the failed extrinsic, if it has failed.
+
+	pub fn get_associated_dispatch_error(&self) -> Option<DispatchError> {
+		self.inner.get_associated_dispatch_error(&self.metadata)
 	}
 
 	/// Checks if the event represents a code update (runtime update).
 	pub fn is_code_update(&self) -> bool {
-		self.pallet_name() == "System" && self.variant_name() == "CodeUpdated"
+		self.inner.is_code_update()
 	}
 }
-
-/// Details for the given event plucked from the metadata.
-// Based on https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L411-L415
-pub struct EventMetadataDetails<'a> {
-	pub pallet: PalletMetadata<'a>,
-	pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
-}
-
-/// This trait is implemented on the statically generated root event type, so that we're able
-/// to decode it properly via a pallet event that impls `DecodeAsMetadata`. This is necessary
-/// becasue the "root event" type is generated using pallet info but doesn't actually exist in the
-/// metadata types, so we have no easy way to decode things into it via type information and need a
-/// little help via codegen.
-// Based on https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L417-L432
-#[doc(hidden)]
-pub trait RootEvent: Sized {
-	/// Given details of the pallet event we want to decode, and the name of the pallet, try to hand
-	/// back a "root event".
-	fn root_event(
-		pallet_bytes: &[u8],
-		pallet_name: &str,
-		pallet_event_ty: u32,
-		metadata: &Metadata,
-	) -> Result<Self, Error>;
-}
diff --git a/node-api/src/events/mod.rs b/node-api/src/events/mod.rs
index d76bf3e85..93bd5c582 100644
--- a/node-api/src/events/mod.rs
+++ b/node-api/src/events/mod.rs
@@ -9,12 +9,14 @@
 //! A representation of a block of events.
 //! This file bases on https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L19-L196
 
-use crate::{error::Error, Metadata, StaticEvent};
+use crate::{error::Error, metadata::PalletMetadata, Metadata, StaticEvent};
 use alloc::{sync::Arc, vec::Vec};
-use codec::{Compact, Decode};
+use codec::{Compact, Decode, Encode};
 
 mod event_details;
+mod raw_event_details;
 pub use event_details::EventDetails;
+pub use raw_event_details::RawEventDetails;
 
 /// A collection of events obtained from a block, bundled with the necessary
 /// information needed to decode and iterate over them.
@@ -42,7 +44,7 @@ impl<Hash: core::fmt::Debug> core::fmt::Debug for Events<Hash> {
 	}
 }
 
-impl<Hash: Copy + Decode> Events<Hash> {
+impl<Hash: Copy + Encode + Decode> Events<Hash> {
 	pub fn new(metadata: Metadata, block_hash: Hash, event_bytes: Vec<u8>) -> Self {
 		// event_bytes is a SCALE encoded vector of events. So, pluck the
 		// compact encoded length from the front, leaving the remaining bytes
@@ -146,6 +148,32 @@ impl<Hash: Copy + Decode> Events<Hash> {
 	}
 }
 
+/// This trait is implemented on the statically generated root event type, so that we're able
+/// to decode it properly via a pallet event that impls `DecodeAsMetadata`. This is necessary
+/// becasue the "root event" type is generated using pallet info but doesn't actually exist in the
+/// metadata types, so we have no easy way to decode things into it via type information and need a
+/// little help via codegen.
+// Based on https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L417-L432
+#[doc(hidden)]
+pub trait RootEvent: Sized {
+	/// Given details of the pallet event we want to decode, and the name of the pallet, try to hand
+	/// back a "root event".
+	fn root_event(
+		pallet_bytes: &[u8],
+		pallet_name: &str,
+		pallet_event_ty: u32,
+		metadata: &Metadata,
+	) -> Result<Self, Error>;
+}
+
+/// Details for the given event plucked from the metadata.
+// Based on https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L411-L415
+#[derive(Clone)]
+pub struct EventMetadataDetails<'a> {
+	pub pallet: PalletMetadata<'a>,
+	pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
diff --git a/node-api/src/events/raw_event_details.rs b/node-api/src/events/raw_event_details.rs
new file mode 100644
index 000000000..251c2e295
--- /dev/null
+++ b/node-api/src/events/raw_event_details.rs
@@ -0,0 +1,860 @@
+// This file bases on subxt (Parity Technologies (UK))
+// https://github.com/paritytech/subxt/
+// And was adapted by Supercomputing Systems AG.
+
+// 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.
+
+//! A representation of a block of events.
+
+use crate::{
+	error::{DispatchError, Error},
+	events::{EventMetadataDetails, RootEvent},
+	metadata::MetadataError,
+	EventDetails, Metadata, Phase, StaticEvent,
+};
+use alloc::{
+	string::{String, ToString},
+	sync::Arc,
+	vec::Vec,
+};
+use codec::{Decode, Encode};
+use log::*;
+use scale_decode::DecodeAsFields;
+use scale_info::PortableRegistry;
+use scale_value::{scale::TypeId, Composite};
+
+/// Raw event details without the associated metadata
+// Based on subxt EventDetails.
+// https://github.com/paritytech/subxt/blob/8413c4d2dd625335b9200dc2289670accdf3391a/subxt/src/events/events_type.rs#L197-L216
+#[derive(Debug, Clone, Encode, Decode, PartialEq)]
+pub struct RawEventDetails<Hash: Decode + Encode> {
+	phase: Phase,
+	/// The index of the event in the list of events in a given block.
+	index: u32,
+	/// Raw event bytes, inlcuding
+	/// - The phase.
+	/// - Pallet and event index.
+	/// - Event fields.
+	/// - Event Topics.
+	bytes: Vec<u8>,
+	/// Raw event field bytes.
+	field_bytes: Vec<u8>,
+	/// Associated Pallet information.
+	pallet_index: u8,
+	pallet_name: String,
+	pallet_bytes: Vec<u8>,
+	/// Associated Variant information.
+	variant_index: u8,
+	variant_name: String,
+	/// Associated Topcis.
+	topics: Vec<Hash>,
+}
+
+impl<Hash: Encode + Decode> RawEventDetails<Hash> {
+	/// When was the event produced?
+	pub fn phase(&self) -> Phase {
+		self.phase
+	}
+
+	/// What index is this event in the stored events for this block.
+	pub fn index(&self) -> u32 {
+		self.index
+	}
+
+	/// The index of the pallet that the event originated from.
+	pub fn pallet_index(&self) -> u8 {
+		// Note: never panics; we expect these bytes to exist
+		// in order that the EventDetails could be created.
+		self.pallet_index
+	}
+
+	/// The index of the event variant that the event originated from.
+	pub fn variant_index(&self) -> u8 {
+		// Note: never panics; we expect these bytes to exist
+		// in order that the EventDetails could be created.
+		self.variant_index
+	}
+
+	/// The name of the pallet from whence the Event originated.
+	pub fn pallet_name(&self) -> &str {
+		&self.pallet_name
+	}
+
+	/// The name of the event (ie the name of the variant that it corresponds to).
+	pub fn variant_name(&self) -> &str {
+		&self.variant_name
+	}
+
+	/// Fetch details from the metadata for this event.
+	pub fn event_metadata<'a>(
+		&'a self,
+		metadata: &'a Metadata,
+	) -> Result<EventMetadataDetails, Error> {
+		let pallet = metadata
+			.pallet_by_index(self.pallet_index())
+			.ok_or(Error::Metadata(MetadataError::PalletIndexNotFound(self.pallet_index())))?;
+		let variant = pallet
+			.event_variant_by_index(self.variant_index())
+			.ok_or(Error::Metadata(MetadataError::VariantIndexNotFound(self.variant_index())))?;
+		let event_metadata = EventMetadataDetails { pallet, variant };
+		self.metadata_sanity_check(&event_metadata)?;
+		Ok(event_metadata)
+	}
+
+	/// Return _all_ of the bytes representing this event, which include, in order:
+	/// - The phase.
+	/// - Pallet and event index.
+	/// - Event fields.
+	/// - Event Topics.
+	pub fn bytes(&self) -> &[u8] {
+		&self.bytes
+	}
+
+	/// Return the bytes representing the fields stored in this event.
+	pub fn field_bytes(&self) -> &[u8] {
+		&self.field_bytes
+	}
+
+	/// Decode and provide the event fields back in the form of a [`scale_value::Composite`]
+	/// type which represents the named or unnamed fields that were present in the event.
+	pub fn field_values(&self, metadata: &Metadata) -> Result<Composite<TypeId>, Error> {
+		let event_metadata = self.event_metadata(metadata)?;
+		self.field_values_inner(&event_metadata, metadata.types())
+	}
+
+	/// Attempt to decode these [`EventDetails`] into a specific static event.
+	/// This targets the fields within the event directly. You can also attempt to
+	/// decode the entirety of the event type (including the pallet and event
+	/// variants) using [`EventDetails::as_root_event()`].
+	pub fn as_event<E: StaticEvent>(&self) -> Result<Option<E>, Error> {
+		if self.pallet_name() == E::PALLET && self.variant_name() == E::EVENT {
+			Ok(Some(E::decode(&mut self.field_bytes())?))
+		} else {
+			Ok(None)
+		}
+	}
+
+	/// Attempt to decode these [`EventDetails`] into a root event type (which includes
+	/// the pallet and event enum variants as well as the event fields). A compatible
+	/// type for this is exposed via static codegen as a root level `Event` type.
+	pub fn as_root_event<E: RootEvent>(&self, metadata: &Metadata) -> Result<E, Error> {
+		let event_metadata = self.event_metadata(metadata)?;
+		self.as_root_event_inner(metadata, &event_metadata)
+	}
+
+	/// Return the topics associated with this event.
+	pub fn topics(&self) -> &[Hash] {
+		&self.topics
+	}
+
+	pub fn pallet_bytes(&self) -> &[u8] {
+		&self.pallet_bytes
+	}
+}
+
+impl<Hash: Encode + Decode> RawEventDetails<Hash> {
+	/// Checks if the extrinsic has failed.
+	pub fn has_failed(&self) -> bool {
+		self.pallet_name() == "System" && self.variant_name() == "ExtrinsicFailed"
+	}
+
+	/// Returns the dispatch error of the failed extrinsic, if it has failed.
+	pub fn get_associated_dispatch_error(&self, metadata: &Metadata) -> Option<DispatchError> {
+		match self.has_failed() {
+			true => Some(
+				DispatchError::decode_from(self.field_bytes(), metadata)
+					.unwrap_or(DispatchError::CannotLookup),
+			),
+			false => None,
+		}
+	}
+
+	/// Checks if the event represents a code update (runtime update).
+	pub fn is_code_update(&self) -> bool {
+		self.pallet_name() == "System" && self.variant_name() == "CodeUpdated"
+	}
+}
+
+impl<Hash: Encode + Decode> From<EventDetails<Hash>> for RawEventDetails<Hash> {
+	fn from(val: EventDetails<Hash>) -> Self {
+		val.to_raw()
+	}
+}
+
+// Private / Crate methods
+impl<Hash: Encode + Decode> RawEventDetails<Hash> {
+	// Attempt to dynamically decode a single event from our events input.
+	pub(crate) fn decode_from(
+		metadata: &Metadata,
+		all_bytes: Arc<[u8]>,
+		start_idx: usize,
+		index: u32,
+	) -> Result<Self, Error> {
+		let input = &mut &all_bytes[start_idx..];
+
+		let phase = Phase::decode(input)?;
+
+		let event_start_idx = all_bytes.len() - input.len();
+
+		let pallet_index = u8::decode(input)?;
+		let variant_index = u8::decode(input)?;
+
+		let event_fields_start_idx = all_bytes.len() - input.len();
+
+		// Get metadata for the event:
+		let event_pallet = metadata.pallet_by_index_err(pallet_index)?;
+		let event_variant = event_pallet
+			.event_variant_by_index(variant_index)
+			.ok_or(MetadataError::VariantIndexNotFound(variant_index))?;
+		let pallet_name = event_pallet.name().to_string();
+		let variant_name = event_variant.name.to_string();
+		debug!("Decoding Event '{}::{}'", &pallet_name, &variant_name);
+
+		// Skip over the bytes belonging to this event.
+		for field_metadata in &event_variant.fields {
+			// Skip over the bytes for this field:
+			scale_decode::visitor::decode_with_visitor(
+				input,
+				field_metadata.ty.id,
+				metadata.types(),
+				scale_decode::visitor::IgnoreVisitor,
+			)
+			.map_err(scale_decode::Error::from)?;
+		}
+
+		// the end of the field bytes.
+		let event_fields_end_idx = all_bytes.len() - input.len();
+
+		// topics come after the event data in EventRecord.
+		let topics = Vec::<Hash>::decode(input)?;
+
+		// what bytes did we skip over in total, including topics.
+		let end_idx = all_bytes.len() - input.len();
+		let pallet_bytes: Vec<u8> = all_bytes[event_start_idx + 1..event_fields_end_idx].into();
+		let pallet_index = all_bytes[event_fields_start_idx - 2];
+		let variant_index = all_bytes[event_fields_start_idx - 1];
+		let bytes: Vec<u8> = all_bytes[start_idx..end_idx].into();
+		let field_bytes: Vec<u8> = all_bytes[event_fields_start_idx..event_fields_end_idx].into();
+
+		Ok(RawEventDetails {
+			phase,
+			index,
+			bytes,
+			field_bytes,
+			pallet_index,
+			pallet_name,
+			pallet_bytes,
+			variant_index,
+			variant_name,
+			topics,
+		})
+	}
+
+	/// Fetch details from the metadata for this event.
+	pub(crate) fn event_metadata_unchecked<'a>(
+		&'a self,
+		metadata: &'a Metadata,
+	) -> EventMetadataDetails {
+		let pallet = metadata
+			.pallet_by_index(self.pallet_index())
+			.expect("event pallet to be found; we did this already during decoding");
+		let variant = pallet
+			.event_variant_by_index(self.variant_index())
+			.expect("event variant to be found; we did this already during decoding");
+
+		EventMetadataDetails { pallet, variant }
+	}
+
+	/// Decode and provide the event fields back in the form of a [`scale_value::Composite`]
+	/// type which represents the named or unnamed fields that were present in the event.
+	pub(crate) fn field_values_unchecked(
+		&self,
+		metadata: &Metadata,
+	) -> Result<Composite<TypeId>, Error> {
+		let event_metadata = self.event_metadata_unchecked(metadata);
+		self.field_values_inner(&event_metadata, metadata.types())
+	}
+
+	/// Attempt to decode these [`EventDetails`] into a root event type (which includes
+	/// the pallet and event enum variants as well as the event fields). A compatible
+	/// type for this is exposed via static codegen as a root level `Event` type.
+	pub(crate) fn as_root_event_unchecked<E: RootEvent>(
+		&self,
+		metadata: &Metadata,
+	) -> Result<E, Error> {
+		let event_metadata = self.event_metadata_unchecked(metadata);
+		self.as_root_event_inner(metadata, &event_metadata)
+	}
+
+	pub(crate) fn as_root_event_inner<E: RootEvent>(
+		&self,
+		metadata: &Metadata,
+		event_metadata: &EventMetadataDetails,
+	) -> Result<E, Error> {
+		let pallet_bytes = self.pallet_bytes();
+		let pallet_event_ty = event_metadata.pallet.event_ty_id().ok_or_else(|| {
+			MetadataError::EventTypeNotFoundInPallet(event_metadata.pallet.index())
+		})?;
+
+		E::root_event(pallet_bytes, self.pallet_name(), pallet_event_ty, metadata)
+	}
+
+	fn metadata_sanity_check(&self, event_metadata: &EventMetadataDetails) -> Result<(), Error> {
+		if event_metadata.pallet.name() != self.pallet_name()
+			|| event_metadata.variant.name != self.variant_name()
+		{
+			return Err(Error::Metadata(MetadataError::MetadataMismatch))
+		}
+		Ok(())
+	}
+
+	fn field_values_inner(
+		&self,
+		event_metadata: &EventMetadataDetails,
+		types: &PortableRegistry,
+	) -> Result<Composite<TypeId>, Error> {
+		let bytes = &mut self.field_bytes();
+		let mut fields = event_metadata
+			.variant
+			.fields
+			.iter()
+			.map(|f| scale_decode::Field::new(f.ty.id, f.name.as_deref()));
+		let decoded = <Composite<TypeId>>::decode_as_fields(bytes, &mut fields, types)?;
+
+		Ok(decoded)
+	}
+}
+
+/// Event related test utilities used outside this module.
+#[cfg(test)]
+pub(crate) mod test_utils {
+	use super::*;
+	use crate::{events::Compact, Events};
+	use codec::Encode;
+	use frame_metadata::{
+		v15::{
+			CustomMetadata, ExtrinsicMetadata, OuterEnums, PalletEventMetadata, PalletMetadata,
+			RuntimeMetadataV15,
+		},
+		RuntimeMetadataPrefixed,
+	};
+	use scale_info::{meta_type, TypeInfo};
+	use sp_core::H256;
+
+	/// An "outer" events enum containing exactly one event.
+	#[derive(
+		Encode,
+		Decode,
+		TypeInfo,
+		Clone,
+		Debug,
+		PartialEq,
+		Eq,
+		scale_encode::EncodeAsType,
+		scale_decode::DecodeAsType,
+	)]
+	pub enum AllEvents<Ev> {
+		Test(Ev),
+	}
+
+	/// This encodes to the same format an event is expected to encode to
+	/// in node System.Events storage.
+	#[derive(Encode)]
+	pub struct EventRecord<E: Encode> {
+		phase: Phase,
+		event: AllEvents<E>,
+		topics: Vec<H256>,
+	}
+
+	impl<E: Encode> EventRecord<E> {
+		/// Create a new event record with the given phase, event, and topics.
+		pub fn new(phase: Phase, event: E, topics: Vec<H256>) -> Self {
+			Self { phase, event: AllEvents::Test(event), topics }
+		}
+	}
+
+	/// Build an EventRecord, which encoded events in the format expected
+	/// to be handed back from storage queries to System.Events.
+	pub fn event_record<E: Encode>(phase: Phase, event: E) -> EventRecord<E> {
+		EventRecord::new(phase, event, vec![])
+	}
+
+	/// Build fake metadata consisting of a single pallet that knows
+	/// about the event type provided.
+	pub fn metadata<E: TypeInfo + 'static>() -> Metadata {
+		// Extrinsic needs to contain at least the generic type parameter "Call"
+		// for the metadata to be valid.
+		// The "Call" type from the metadata is used to decode extrinsics.
+		// In reality, the extrinsic type has "Call", "Address", "Extra", "Signature" generic types.
+		#[allow(unused)]
+		#[derive(TypeInfo)]
+		struct ExtrinsicType<Call> {
+			call: Call,
+		}
+		// Because this type is used to decode extrinsics, we expect this to be a TypeDefVariant.
+		// Each pallet must contain one single variant.
+		#[allow(unused)]
+		#[derive(TypeInfo)]
+		enum RuntimeCall {
+			PalletName(Pallet),
+		}
+		// The calls of the pallet.
+		#[allow(unused)]
+		#[derive(TypeInfo)]
+		enum Pallet {
+			#[allow(unused)]
+			SomeCall,
+		}
+
+		let pallets = vec![PalletMetadata {
+			name: "Test",
+			storage: None,
+			calls: None,
+			event: Some(PalletEventMetadata { ty: meta_type::<E>() }),
+			constants: vec![],
+			error: None,
+			index: 0,
+			docs: vec![],
+		}];
+
+		let extrinsic = ExtrinsicMetadata {
+			version: 0,
+			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::<()>(),
+				event_enum_ty: meta_type::<AllEvents<E>>(),
+				error_enum_ty: meta_type::<()>(),
+			},
+			CustomMetadata { map: Default::default() },
+		);
+		let runtime_metadata: RuntimeMetadataPrefixed = meta.into();
+		Metadata::try_from(runtime_metadata).unwrap()
+	}
+
+	/// Build an `Events` object for test purposes, based on the details provided,
+	/// and with a default block hash.
+	pub fn events<E: Decode + Encode>(
+		metadata: Metadata,
+		event_records: Vec<EventRecord<E>>,
+	) -> Events<H256> {
+		let num_events = event_records.len() as u32;
+		let mut event_bytes = Vec::new();
+		for ev in event_records {
+			ev.encode_to(&mut event_bytes);
+		}
+		events_raw(metadata, event_bytes, num_events)
+	}
+
+	/// Much like [`events`], but takes pre-encoded events and event count, so that we can
+	/// mess with the bytes in tests if we need to.
+	pub fn events_raw(metadata: Metadata, event_bytes: Vec<u8>, num_events: u32) -> Events<H256> {
+		// Prepend compact encoded length to event bytes:
+		let mut all_event_bytes = Compact(num_events).encode();
+		all_event_bytes.extend(event_bytes);
+		Events::new(metadata, H256::default(), all_event_bytes)
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	use super::{
+		test_utils::{event_record, events, events_raw, EventRecord},
+		*,
+	};
+	use codec::Encode;
+	use scale_info::TypeInfo;
+	use scale_value::Value;
+	use sp_core::H256;
+
+	/// Build a fake wrapped metadata.
+	fn metadata<E: TypeInfo + 'static>() -> Metadata {
+		test_utils::metadata::<E>()
+	}
+
+	/// [`RawEventDetails`] can be annoying to test, because it contains
+	/// type info in the decoded field Values. Strip that here so that
+	/// we can compare fields more easily.
+	#[derive(Debug, PartialEq, Eq, Clone)]
+	pub struct TestRawEventDetails {
+		pub phase: Phase,
+		pub index: u32,
+		pub pallet: String,
+		pub pallet_index: u8,
+		pub variant: String,
+		pub variant_index: u8,
+		pub fields: Vec<scale_value::Value>,
+	}
+
+	/// Compare some actual [`RawEventDetails`] with a hand-constructed
+	/// (probably) [`TestRawEventDetails`].
+	pub fn assert_raw_events_match(
+		actual: RawEventDetails<H256>,
+		expected: TestRawEventDetails,
+		metadata: &Metadata,
+	) {
+		let actual_fields_no_context: Vec<_> = actual
+			.field_values(metadata)
+			.expect("can decode field values (2)")
+			.into_values()
+			.map(|value| value.remove_context())
+			.collect();
+
+		// Check each of the other fields:
+		assert_eq!(actual.phase(), expected.phase);
+		assert_eq!(actual.index(), expected.index);
+		assert_eq!(actual.pallet_name(), expected.pallet);
+		assert_eq!(actual.pallet_index(), expected.pallet_index);
+		assert_eq!(actual.variant_name(), expected.variant);
+		assert_eq!(actual.variant_index(), expected.variant_index);
+		assert_eq!(actual_fields_no_context, expected.fields);
+	}
+
+	#[test]
+	fn dynamically_decode_single_event() {
+		#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
+		enum Event {
+			A(u8, bool, Vec<String>),
+		}
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode our events in the format we expect back from a node, and
+		// construst an Events object to iterate them:
+		let event = Event::A(1, true, vec!["Hi".into()]);
+		let events = events::<Event>(
+			metadata.clone(),
+			vec![event_record(Phase::ApplyExtrinsic(123), event)],
+		);
+
+		let mut event_details_iter = events.iter();
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				phase: Phase::ApplyExtrinsic(123),
+				index: 0,
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "A".to_string(),
+				variant_index: 0,
+				fields: vec![
+					Value::u128(1),
+					Value::bool(true),
+					Value::unnamed_composite(vec![Value::string("Hi")]),
+				],
+			},
+			&metadata,
+		);
+		assert!(event_details_iter.next().is_none());
+	}
+
+	#[test]
+	fn dynamically_decode_multiple_events() {
+		#[derive(Clone, Copy, Debug, PartialEq, Decode, Encode, TypeInfo)]
+		enum Event {
+			A(u8),
+			B(bool),
+		}
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode our events in the format we expect back from a node, and
+		// construst an Events object to iterate them:
+		let event1 = Event::A(1);
+		let event2 = Event::B(true);
+		let event3 = Event::A(234);
+
+		let events = events::<Event>(
+			metadata.clone(),
+			vec![
+				event_record(Phase::Initialization, event1),
+				event_record(Phase::ApplyExtrinsic(123), event2),
+				event_record(Phase::Finalization, event3),
+			],
+		);
+
+		let mut event_details_iter = events.iter();
+
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 0,
+				phase: Phase::Initialization,
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "A".to_string(),
+				variant_index: 0,
+				fields: vec![Value::u128(1)],
+			},
+			&metadata,
+		);
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 1,
+				phase: Phase::ApplyExtrinsic(123),
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "B".to_string(),
+				variant_index: 1,
+				fields: vec![Value::bool(true)],
+			},
+			&metadata,
+		);
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 2,
+				phase: Phase::Finalization,
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "A".to_string(),
+				variant_index: 0,
+				fields: vec![Value::u128(234)],
+			},
+			&metadata,
+		);
+		assert!(event_details_iter.next().is_none());
+	}
+
+	#[test]
+	fn dynamically_decode_multiple_events_until_error() {
+		#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
+		enum Event {
+			A(u8),
+			B(bool),
+		}
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode 2 events:
+		let mut event_bytes = vec![];
+		event_record(Phase::Initialization, Event::A(1)).encode_to(&mut event_bytes);
+		event_record(Phase::ApplyExtrinsic(123), Event::B(true)).encode_to(&mut event_bytes);
+
+		// Push a few naff bytes to the end (a broken third event):
+		event_bytes.extend_from_slice(&[3, 127, 45, 0, 2]);
+
+		// Encode our events in the format we expect back from a node, and
+		// construst an Events object to iterate them:
+		let events = events_raw(
+			metadata.clone(),
+			event_bytes,
+			3, // 2 "good" events, and then it'll hit the naff bytes.
+		);
+		let mut event_details_iter = events.iter();
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 0,
+				phase: Phase::Initialization,
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "A".to_string(),
+				variant_index: 0,
+				fields: vec![Value::u128(1)],
+			},
+			&metadata,
+		);
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 1,
+				phase: Phase::ApplyExtrinsic(123),
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "B".to_string(),
+				variant_index: 1,
+				fields: vec![Value::bool(true)],
+			},
+			&metadata,
+		);
+
+		// We'll hit an error trying to decode the third event:
+		assert!(event_details_iter.next().unwrap().is_err());
+		// ... and then "None" from then on.
+		assert!(event_details_iter.next().is_none());
+		assert!(event_details_iter.next().is_none());
+	}
+
+	#[test]
+	fn compact_event_field() {
+		#[derive(Clone, Debug, PartialEq, Encode, Decode, TypeInfo)]
+		enum Event {
+			A(#[codec(compact)] u32),
+		}
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode our events in the format we expect back from a node, and
+		// construst an Events object to iterate them:
+		let events =
+			events::<Event>(metadata.clone(), vec![event_record(Phase::Finalization, Event::A(1))]);
+
+		// Dynamically decode:
+		let mut event_details_iter = events.iter();
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 0,
+				phase: Phase::Finalization,
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "A".to_string(),
+				variant_index: 0,
+				fields: vec![Value::u128(1)],
+			},
+			&metadata,
+		);
+		assert!(event_details_iter.next().is_none());
+	}
+
+	#[test]
+	fn compact_wrapper_struct_field() {
+		#[derive(Clone, Decode, Debug, PartialEq, Encode, TypeInfo)]
+		enum Event {
+			A(#[codec(compact)] CompactWrapper),
+		}
+
+		#[derive(Clone, Decode, Debug, PartialEq, codec::CompactAs, Encode, TypeInfo)]
+		struct CompactWrapper(u64);
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode our events in the format we expect back from a node, and
+		// construct an Events object to iterate them:
+		let events = events::<Event>(
+			metadata.clone(),
+			vec![event_record(Phase::Finalization, Event::A(CompactWrapper(1)))],
+		);
+
+		// Dynamically decode:
+		let mut event_details_iter = events.iter();
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 0,
+				phase: Phase::Finalization,
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "A".to_string(),
+				variant_index: 0,
+				fields: vec![Value::unnamed_composite(vec![Value::u128(1)])],
+			},
+			&metadata,
+		);
+		assert!(event_details_iter.next().is_none());
+	}
+
+	#[test]
+	fn event_containing_explicit_index() {
+		#[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo)]
+		#[repr(u8)]
+		#[allow(trivial_numeric_casts, clippy::unnecessary_cast)] // required because the Encode derive produces a warning otherwise
+		pub enum MyType {
+			B = 10u8,
+		}
+
+		#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
+		enum Event {
+			A(MyType),
+		}
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode our events in the format we expect back from a node, and
+		// construct an Events object to iterate them:
+		let events = events::<Event>(
+			metadata.clone(),
+			vec![event_record(Phase::Finalization, Event::A(MyType::B))],
+		);
+
+		// Dynamically decode:
+		let mut event_details_iter = events.iter();
+		assert_raw_events_match(
+			event_details_iter.next().unwrap().unwrap().to_raw(),
+			TestRawEventDetails {
+				index: 0,
+				phase: Phase::Finalization,
+				pallet: "Test".to_string(),
+				pallet_index: 0,
+				variant: "A".to_string(),
+				variant_index: 0,
+				fields: vec![Value::unnamed_variant("B", vec![])],
+			},
+			&metadata,
+		);
+		assert!(event_details_iter.next().is_none());
+	}
+
+	#[test]
+	fn topics() {
+		#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo, scale_decode::DecodeAsType)]
+		enum Event {
+			A(u8, bool, Vec<String>),
+		}
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode our events in the format we expect back from a node, and
+		// construct an Events object to iterate them:
+		let event = Event::A(1, true, vec!["Hi".into()]);
+		let topics = vec![H256::from_low_u64_le(123), H256::from_low_u64_le(456)];
+		let events = events::<Event>(
+			metadata,
+			vec![EventRecord::new(Phase::ApplyExtrinsic(123), event, topics.clone())],
+		);
+
+		let ev = events
+			.iter()
+			.next()
+			.expect("one event expected")
+			.expect("event should be extracted OK");
+
+		assert_eq!(topics, ev.topics());
+	}
+
+	#[test]
+	fn encode_decode() {
+		#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo, scale_decode::DecodeAsType)]
+		enum Event {
+			A(#[codec(compact)] u32),
+		}
+
+		// Create fake metadata that knows about our single event, above:
+		let metadata = metadata::<Event>();
+
+		// Encode our events in the format we expect back from a node, and
+		// construst an Events object to iterate them:
+		let events =
+			events::<Event>(metadata.clone(), vec![event_record(Phase::Finalization, Event::A(1))]);
+
+		// Dynamically decode:
+		let raw_event_details = events.iter().next().unwrap().unwrap().to_raw();
+
+		// Statically Encode/Decode:
+		let encoded = raw_event_details.encode();
+		let decoded = RawEventDetails::decode(&mut encoded.as_slice()).unwrap();
+
+		assert_eq!(raw_event_details, decoded);
+	}
+}
diff --git a/node-api/src/lib.rs b/node-api/src/lib.rs
index c7aeefe0c..558af9559 100644
--- a/node-api/src/lib.rs
+++ b/node-api/src/lib.rs
@@ -22,7 +22,7 @@ use alloc::{borrow::ToOwned, vec::Vec};
 use codec::{Decode, Encode};
 
 pub use alloc::{collections::BTreeMap, vec};
-pub use events::{EventDetails, Events};
+pub use events::{EventDetails, Events, RawEventDetails};
 pub use metadata::{Metadata, MetadataError};
 pub use scale_decode::DecodeAsType;
 
diff --git a/node-api/src/metadata/error.rs b/node-api/src/metadata/error.rs
index 0169592d3..f9d7e45df 100644
--- a/node-api/src/metadata/error.rs
+++ b/node-api/src/metadata/error.rs
@@ -41,6 +41,8 @@ pub enum MetadataError {
 	VariantIndexNotFound(u8),
 	/// Api is not in metadata.
 	RuntimeApiNotFound(String),
+	/// Exptected a different type of Metadata. Has there been a runtime upgrade inbetween?
+	MetadataMismatch,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Encode, Decode)]
diff --git a/src/api/api_client.rs b/src/api/api_client.rs
index 9c5df7072..a5541d413 100644
--- a/src/api/api_client.rs
+++ b/src/api/api_client.rs
@@ -29,6 +29,7 @@ use frame_metadata::RuntimeMetadataPrefixed;
 use log::{debug, info};
 use sp_core::Bytes;
 use sp_version::RuntimeVersion;
+
 /// Api to talk with substrate-nodes
 ///
 /// It is generic over the `Request` trait, so you can use any rpc-backend you like.
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 1be571acd..42ba95870 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -15,9 +15,9 @@
 
 */
 
-use ac_node_api::EventDetails;
+use ac_node_api::events::RawEventDetails;
 use alloc::{string::String, vec::Vec};
-use codec::Decode;
+use codec::{Decode, Encode};
 use serde::{Deserialize, Serialize};
 use sp_core::Bytes;
 
@@ -36,8 +36,8 @@ pub mod runtime_api;
 
 /// Extrinsic report returned upon a submit_and_watch request.
 /// Holds as much information as available.
-#[derive(Debug, Clone)]
-pub struct ExtrinsicReport<Hash: Decode> {
+#[derive(Debug, Clone, Encode, Decode, PartialEq)]
+pub struct ExtrinsicReport<Hash: Encode + Decode> {
 	// Hash of the extrinsic.
 	pub extrinsic_hash: Hash,
 	// Block hash of the block the extrinsic was included in.
@@ -48,15 +48,15 @@ pub struct ExtrinsicReport<Hash: Decode> {
 	// Events associated to the extrinsic.
 	// Only available if explicitly stated, because
 	// extra node queries are necessary to fetch the events.
-	pub events: Option<Vec<EventDetails<Hash>>>,
+	pub events: Option<Vec<RawEventDetails<Hash>>>,
 }
 
-impl<Hash: Decode> ExtrinsicReport<Hash> {
+impl<Hash: Encode + Decode> ExtrinsicReport<Hash> {
 	pub fn new(
 		extrinsic_hash: Hash,
 		block_hash: Option<Hash>,
 		status: TransactionStatus<Hash, Hash>,
-		events: Option<Vec<EventDetails<Hash>>>,
+		events: Option<Vec<RawEventDetails<Hash>>>,
 	) -> Self {
 		Self { extrinsic_hash, block_hash, status, events }
 	}
@@ -89,9 +89,9 @@ pub enum UnexpectedTxStatus {
 // Copied from `sc-transaction-pool`
 // (https://github.com/paritytech/substrate/blob/dddfed3d9260cf03244f15ba3db4edf9af7467e9/client/transaction-pool/api/src/lib.rs)
 // as the library is not no-std compatible
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)]
 #[serde(rename_all = "camelCase")]
-pub enum TransactionStatus<Hash, BlockHash> {
+pub enum TransactionStatus<Hash: Encode + Decode, BlockHash: Encode + Decode> {
 	/// Transaction is part of the future queue.
 	Future,
 	/// Transaction is part of the ready queue.
@@ -116,7 +116,7 @@ pub enum TransactionStatus<Hash, BlockHash> {
 	Invalid,
 }
 
-impl<Hash, BlockHash> TransactionStatus<Hash, BlockHash> {
+impl<Hash: Encode + Decode, BlockHash: Encode + Decode> TransactionStatus<Hash, BlockHash> {
 	pub fn as_u8(&self) -> u8 {
 		match self {
 			TransactionStatus::Future => 0,
@@ -302,4 +302,18 @@ mod tests {
 		assert!(TransactionStatus::Dropped.reached_status(status));
 		assert!(TransactionStatus::Invalid.reached_status(status));
 	}
+
+	#[test]
+	fn encode_decode_extrinsic_report() {
+		let hash = H256::random();
+		let block_hash = H256::random();
+		let status = TransactionStatus::InBlock(block_hash.clone());
+		// RawEventDetails Encoding / Decoding is already tested separately, so we don't need to retest here.
+		let report = ExtrinsicReport::new(hash, Some(block_hash), status, None);
+
+		let encoded = report.encode();
+		let decoded = ExtrinsicReport::<H256>::decode(&mut encoded.as_slice()).unwrap();
+
+		assert_eq!(report, decoded);
+	}
 }
diff --git a/src/api/rpc_api/author.rs b/src/api/rpc_api/author.rs
index 1177fb2dc..dac237f9a 100644
--- a/src/api/rpc_api/author.rs
+++ b/src/api/rpc_api/author.rs
@@ -85,7 +85,7 @@ where
 #[maybe_async::maybe_async(?Send)]
 pub trait SubmitAndWatch {
 	type Client: Subscribe;
-	type Hash: DeserializeOwned + Decode;
+	type Hash: DeserializeOwned + Decode + Encode;
 
 	/// Submit an extrinsic an return a Subscription
 	/// to watch the extrinsic progress.
@@ -279,11 +279,14 @@ where
 		let extrinsic_events =
 			self.fetch_events_for_extrinsic(block_hash, report.extrinsic_hash).await?;
 
-		// Ensure that the extrinsic has been successful. If not, return an error.
+		// Ensure the extrinsic was successful. If not, return an error.
 		for event in &extrinsic_events {
-			event.check_if_failed()?;
+			if let Some(dispatch_error) = event.get_associated_dispatch_error() {
+				return Err(Error::Dispatch(dispatch_error))
+			}
 		}
-		report.events = Some(extrinsic_events);
+		report.events = Some(extrinsic_events.into_iter().map(|event| event.to_raw()).collect());
+
 		Ok(report)
 	}
 
diff --git a/src/api/rpc_api/events.rs b/src/api/rpc_api/events.rs
index 7956eb5de..9bc8d17b5 100644
--- a/src/api/rpc_api/events.rs
+++ b/src/api/rpc_api/events.rs
@@ -22,7 +22,7 @@ use ac_primitives::config::Config;
 #[cfg(not(feature = "sync-api"))]
 use alloc::boxed::Box;
 use alloc::{vec, vec::Vec};
-use codec::Decode;
+use codec::{Decode, Encode};
 use core::marker::PhantomData;
 use log::*;
 use serde::de::DeserializeOwned;
@@ -34,7 +34,7 @@ pub type EventSubscriptionFor<Client, Hash> =
 
 #[maybe_async::maybe_async(?Send)]
 pub trait FetchEvents {
-	type Hash: Decode;
+	type Hash: Encode + Decode;
 
 	/// Fetch all block events from node for the given block hash.
 	async fn fetch_events_from_block(&self, block_hash: Self::Hash) -> Result<Events<Self::Hash>>;
@@ -100,7 +100,7 @@ impl<Subscription, Hash> EventSubscription<Subscription, Hash> {
 
 impl<Subscription, Hash> EventSubscription<Subscription, Hash>
 where
-	Hash: DeserializeOwned + Copy + Decode,
+	Hash: DeserializeOwned + Copy + Encode + Decode,
 	Subscription: HandleSubscription<StorageChangeSet<Hash>>,
 {
 	/// Wait for the next value from the internal subscription.
diff --git a/src/api/rpc_api/runtime_update.rs b/src/api/rpc_api/runtime_update.rs
index 2f15837b5..10d516515 100644
--- a/src/api/rpc_api/runtime_update.rs
+++ b/src/api/rpc_api/runtime_update.rs
@@ -12,7 +12,7 @@
 */
 use crate::{api::Error, rpc::Subscribe, rpc_api::EventSubscriptionFor, Result};
 use alloc::sync::Arc;
-use codec::Decode;
+use codec::{Decode, Encode};
 use core::sync::atomic::{AtomicBool, Ordering};
 use serde::de::DeserializeOwned;
 
@@ -28,7 +28,7 @@ where
 
 impl<Hash, Client> RuntimeUpdateDetector<Hash, Client>
 where
-	Hash: DeserializeOwned + Copy + Decode,
+	Hash: DeserializeOwned + Copy + Encode + Decode,
 	Client: Subscribe,
 {
 	pub fn new(subscription: EventSubscriptionFor<Client, Hash>) -> Self {


Test patch:
diff --git a/testing/async/examples/author_tests.rs b/testing/async/examples/author_tests.rs
index 37bb50c92..d2b3c85ef 100644
--- a/testing/async/examples/author_tests.rs
+++ b/testing/async/examples/author_tests.rs
@@ -19,7 +19,7 @@ use kitchensink_runtime::{AccountId, BalancesCall, RuntimeCall};
 use sp_core::{Encode, H256};
 use sp_keyring::AccountKeyring;
 use substrate_api_client::{
-	ac_node_api::EventDetails,
+	ac_node_api::RawEventDetails,
 	ac_primitives::{
 		AssetRuntimeConfig, Config, ExtrinsicSigner as GenericExtrinsicSigner, SignExtrinsic,
 	},
@@ -175,7 +175,7 @@ async fn test_submit_and_watch_extrinsic_until_in_block_without_events(
 	assert!(report.events.is_none());
 }
 
-fn assert_associated_events_match_expected(events: Vec<EventDetails<Hash>>) {
+fn assert_associated_events_match_expected(events: Vec<RawEventDetails<Hash>>) {
 	// First event
 	assert_eq!(events[0].pallet_name(), "Balances");
 	assert_eq!(events[0].variant_name(), "Withdraw");