diff --git a/Cargo.lock b/Cargo.lock index 64aa9fc637..e9152cc762 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1545,6 +1545,7 @@ dependencies = [ "scale-info", "sp-core", "sp-keyring", + "sp-runtime", "subxt", "subxt-codegen", "syn 1.0.109", diff --git a/subxt/src/blocks/block_types.rs b/subxt/src/blocks/block_types.rs index 068802d8de..4f994cd1e4 100644 --- a/subxt/src/blocks/block_types.rs +++ b/subxt/src/blocks/block_types.rs @@ -72,7 +72,7 @@ where let block_hash = self.header.hash(); let block_details = match self.client.rpc().block(Some(block_hash)).await? { Some(block) => block, - None => return Err(BlockError::block_hash_not_found(block_hash).into()), + None => return Err(BlockError::not_found(block_hash).into()), }; Ok(BlockBody::new( diff --git a/subxt/src/blocks/blocks_client.rs b/subxt/src/blocks/blocks_client.rs index 5cccfb9904..0b3c0b660f 100644 --- a/subxt/src/blocks/blocks_client.rs +++ b/subxt/src/blocks/blocks_client.rs @@ -66,7 +66,7 @@ where let block_header = match client.rpc().header(Some(block_hash)).await? { Some(header) => header, - None => return Err(BlockError::block_hash_not_found(block_hash).into()), + None => return Err(BlockError::not_found(block_hash).into()), }; Ok(Block::new(block_header, client)) diff --git a/subxt/src/error.rs b/subxt/src/error.rs deleted file mode 100644 index d49c2ad9ad..0000000000 --- a/subxt/src/error.rs +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2019-2022 Parity Technologies (UK) Ltd. -// This file is dual-licensed as Apache-2.0 or GPL-3.0. -// see LICENSE for license details. - -//! Types representing the errors that can be returned. - -use crate::metadata::Metadata; -use codec::Decode; -use core::fmt::Debug; -use scale_info::TypeDef; -use std::borrow::Cow; - -// Re-expose the errors we use from other crates here: -pub use crate::metadata::{InvalidMetadataError, MetadataError}; -pub use scale_decode::Error as DecodeError; -pub use scale_encode::Error as EncodeError; - -/// The underlying error enum, generic over the type held by the `Runtime` -/// variant. Prefer to use the [`Error`] and [`Error`] aliases over -/// using this type directly. -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// Io error. - #[error("Io error: {0}")] - Io(#[from] std::io::Error), - /// Codec error. - #[error("Scale codec error: {0}")] - Codec(#[from] codec::Error), - /// Rpc error. - #[error("Rpc error: {0}")] - Rpc(#[from] RpcError), - /// Serde serialization error - #[error("Serde json error: {0}")] - Serialization(#[from] serde_json::error::Error), - /// Invalid metadata error - #[error("Invalid Metadata: {0}")] - InvalidMetadata(#[from] InvalidMetadataError), - /// Invalid metadata error - #[error("Metadata: {0}")] - Metadata(#[from] MetadataError), - /// Runtime error. - #[error("Runtime error: {0:?}")] - Runtime(DispatchError), - /// Error decoding to a [`crate::dynamic::Value`]. - #[error("Error decoding into dynamic value: {0}")] - Decode(#[from] DecodeError), - /// Error encoding from a [`crate::dynamic::Value`]. - #[error("Error encoding from dynamic value: {0}")] - Encode(#[from] EncodeError), - /// Transaction progress error. - #[error("Transaction error: {0}")] - Transaction(#[from] TransactionError), - /// Block related error. - #[error("Block error: {0}")] - Block(#[from] BlockError), - /// An error encoding a storage address. - #[error("Error encoding storage address: {0}")] - StorageAddress(#[from] StorageAddressError), - /// Other error. - #[error("Other error: {0}")] - Other(String), -} - -impl From<&str> for Error { - fn from(error: &str) -> Self { - Error::Other(error.into()) - } -} - -impl From for Error { - fn from(error: String) -> Self { - Error::Other(error) - } -} - -impl From for Error { - fn from(error: DispatchError) -> Self { - Error::Runtime(error) - } -} - -/// An RPC error. Since we are generic over the RPC client that is used, -/// the error is boxed and could be casted. -#[derive(Debug, thiserror::Error)] -pub enum RpcError { - // Dev note: We need the error to be safely sent between threads - // for `subscribe_to_block_headers_filling_in_gaps` and friends. - /// Error related to the RPC client. - #[error("RPC error: {0}")] - ClientError(Box), - /// The RPC subscription dropped. - #[error("RPC error: subscription dropped.")] - SubscriptionDropped, -} - -/// This is our attempt to decode a runtime DispatchError. We either -/// successfully decode it into a [`ModuleError`], or we fail and keep -/// hold of the bytes, which we can attempt to decode if we have an -/// appropriate static type to hand. -#[derive(Debug, thiserror::Error)] -pub enum DispatchError { - /// An error was emitted from a specific pallet/module. - #[error("Module error: {0}")] - Module(ModuleError), - /// Some other error was emitted. - #[error("Undecoded dispatch error: {0:?}")] - Other(Vec), -} - -impl DispatchError { - /// Attempt to decode a runtime DispatchError, returning either the [`ModuleError`] it decodes - /// to, along with additional details on the error, or returning the raw bytes if it could not - /// be decoded. - pub fn decode_from<'a>(bytes: impl Into>, metadata: &Metadata) -> Self { - let bytes = bytes.into(); - - let dispatch_error_ty_id = match metadata.dispatch_error_ty() { - Some(id) => id, - None => { - tracing::warn!( - "Can't decode error: sp_runtime::DispatchError was not found in Metadata" - ); - return DispatchError::Other(bytes.into_owned()); - } - }; - - let dispatch_error_ty = match metadata.types().resolve(dispatch_error_ty_id) { - Some(ty) => ty, - None => { - tracing::warn!("Can't decode error: sp_runtime::DispatchError type ID doesn't resolve to a known type"); - return DispatchError::Other(bytes.into_owned()); - } - }; - - let variant = match dispatch_error_ty.type_def() { - TypeDef::Variant(var) => var, - _ => { - tracing::warn!( - "Can't decode error: sp_runtime::DispatchError type is not a Variant" - ); - return DispatchError::Other(bytes.into_owned()); - } - }; - - let module_variant_idx = variant - .variants() - .iter() - .find(|v| v.name() == "Module") - .map(|v| v.index()); - let module_variant_idx = match module_variant_idx { - Some(idx) => idx, - None => { - tracing::warn!("Can't decode error: sp_runtime::DispatchError does not have a 'Module' variant"); - return DispatchError::Other(bytes.into_owned()); - } - }; - - // If the error bytes don't correspond to a ModuleError, just return the bytes. - // This is perfectly reasonable and expected, so no logging. - if bytes[0] != module_variant_idx { - return DispatchError::Other(bytes.into_owned()); - } - - // The remaining bytes are the module error, all being well: - let bytes = &bytes[1..]; - - // The oldest and second oldest type of error decode to this shape: - #[derive(Decode)] - struct LegacyModuleError { - index: u8, - error: u8, - } - - // The newer case expands the error for forward compat: - #[derive(Decode)] - struct CurrentModuleError { - index: u8, - error: [u8; 4], - } - - // try to decode into the new shape, or the old if that doesn't work - let err = match CurrentModuleError::decode(&mut &*bytes) { - Ok(e) => e, - Err(_) => { - let old_e = match LegacyModuleError::decode(&mut &*bytes) { - Ok(err) => err, - Err(_) => { - tracing::warn!("Can't decode error: sp_runtime::DispatchError does not match known formats"); - return DispatchError::Other(bytes.to_vec()); - } - }; - CurrentModuleError { - index: old_e.index, - error: [old_e.error, 0, 0, 0], - } - } - }; - - let error_details = match metadata.error(err.index, err.error[0]) { - Ok(details) => details, - Err(_) => { - tracing::warn!("Can't decode error: sp_runtime::DispatchError::Module details do not match known information"); - return DispatchError::Other(bytes.to_vec()); - } - }; - - DispatchError::Module(ModuleError { - pallet: error_details.pallet().to_string(), - error: error_details.error().to_string(), - description: error_details.docs().to_vec(), - error_data: ModuleErrorData { - pallet_index: err.index, - error: err.error, - }, - }) - } -} - -/// Block error -#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)] -pub enum BlockError { - /// The block - #[error("Could not find a block with hash {0} (perhaps it was on a non-finalized fork?)")] - BlockHashNotFound(String), -} - -impl BlockError { - /// Produce an error that a block with the given hash cannot be found. - pub fn block_hash_not_found(hash: impl AsRef<[u8]>) -> BlockError { - let hash = format!("0x{}", hex::encode(hash)); - BlockError::BlockHashNotFound(hash) - } -} - -/// Transaction error. -#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)] -pub enum TransactionError { - /// The finality subscription expired (after ~512 blocks we give up if the - /// block hasn't yet been finalized). - #[error("The finality subscription expired")] - FinalitySubscriptionTimeout, - /// The block hash that the transaction was added to could not be found. - /// This is probably because the block was retracted before being finalized. - #[error("The block containing the transaction can no longer be found (perhaps it was on a non-finalized fork?)")] - BlockHashNotFound, -} - -/// Details about a module error that has occurred. -#[derive(Clone, Debug, thiserror::Error)] -#[error("{pallet}: {error}\n\n{}", .description.join("\n"))] -pub struct ModuleError { - /// The name of the pallet that the error came from. - pub pallet: String, - /// The name of the error. - pub error: String, - /// A description of the error. - pub description: Vec, - /// A byte representation of the error. - pub error_data: ModuleErrorData, -} - -/// The error details about a module error that has occurred. -/// -/// **Note**: Structure used to obtain the underlying bytes of a ModuleError. -#[derive(Clone, Debug, thiserror::Error)] -#[error("Pallet index {pallet_index}: raw error: {error:?}")] -pub struct ModuleErrorData { - /// Index of the pallet that the error came from. - pub pallet_index: u8, - /// Raw error bytes. - pub error: [u8; 4], -} - -impl ModuleErrorData { - /// Obtain the error index from the underlying byte data. - pub fn error_index(&self) -> u8 { - // Error index is utilized as the first byte from the error array. - self.error[0] - } -} - -/// Something went wrong trying to encode a storage address. -#[derive(Clone, Debug, thiserror::Error)] -pub enum StorageAddressError { - /// Storage map type must be a composite type. - #[error("Storage map type must be a composite type")] - MapTypeMustBeTuple, - /// Storage lookup does not have the expected number of keys. - #[error("Storage lookup requires {expected} keys but got {actual} keys")] - WrongNumberOfKeys { - /// The actual number of keys needed, based on the metadata. - actual: usize, - /// The number of keys provided in the storage address. - expected: usize, - }, - /// Storage lookup requires a type that wasn't found in the metadata. - #[error("Storage lookup requires type {0} to exist in the metadata, but it was not found")] - TypeNotFound(u32), - /// This storage entry in the metadata does not have the correct number of hashers to fields. - #[error("Storage entry in metadata does not have the correct number of hashers to fields")] - WrongNumberOfHashers { - /// The number of hashers in the metadata for this storage entry. - hashers: usize, - /// The number of fields in the metadata for this storage entry. - fields: usize, - }, -} diff --git a/subxt/src/error/dispatch_error.rs b/subxt/src/error/dispatch_error.rs new file mode 100644 index 0000000000..2bc6b21ddb --- /dev/null +++ b/subxt/src/error/dispatch_error.rs @@ -0,0 +1,303 @@ +// Copyright 2019-2022 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 the dispatch error; an error returned when +//! something fails in trying to submit/execute a transaction. + +use crate::metadata::{DecodeWithMetadata, Metadata}; +use core::fmt::Debug; +use scale_decode::visitor::DecodeAsTypeResult; +use std::borrow::Cow; + +/// An error dispatching a transaction. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum DispatchError { + /// Some error occurred. + #[error("Some unknown error occurred.")] + Other, + /// Failed to lookup some data. + #[error("Failed to lookup some data.")] + CannotLookup, + /// A bad origin. + #[error("Bad origin.")] + BadOrigin, + /// A custom error in a module. + #[error("Pallet error: {0}")] + Module(ModuleError), + /// At least one consumer is remaining so the account cannot be destroyed. + #[error("At least one consumer is remaining so the account cannot be destroyed.")] + ConsumerRemaining, + /// There are no providers so the account cannot be created. + #[error("There are no providers so the account cannot be created.")] + NoProviders, + /// There are too many consumers so the account cannot be created. + #[error("There are too many consumers so the account cannot be created.")] + TooManyConsumers, + /// An error to do with tokens. + #[error("Token error: {0}")] + Token(TokenError), + /// An arithmetic error. + #[error("Arithmetic error: {0}")] + Arithmetic(ArithmeticError), + /// The number of transactional layers has been reached, or we are not in a transactional layer. + #[error("Transactional error: {0}")] + Transactional(TransactionalError), + /// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate. + #[error( + "Resources exhausted, e.g. attempt to read/write data which is too large to manipulate." + )] + Exhausted, + /// The state is corrupt; this is generally not going to fix itself. + #[error("The state is corrupt; this is generally not going to fix itself.")] + Corruption, + /// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later. + #[error( + "Some resource (e.g. a preimage) is unavailable right now. This might fix itself later." + )] + Unavailable, +} + +/// An error relating to tokens when dispatching a transaction. +#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum TokenError { + /// Funds are unavailable. + #[error("Funds are unavailable.")] + FundsUnavailable, + /// Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved. + #[error("Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved.")] + OnlyProvider, + /// Account cannot exist with the funds that would be given. + #[error("Account cannot exist with the funds that would be given.")] + BelowMinimum, + /// Account cannot be created. + #[error("Account cannot be created.")] + CannotCreate, + /// The asset in question is unknown. + #[error("The asset in question is unknown.")] + UnknownAsset, + /// Funds exist but are frozen. + #[error("Funds exist but are frozen.")] + Frozen, + /// Operation is not supported by the asset. + #[error("Operation is not supported by the asset.")] + Unsupported, + /// Account cannot be created for a held balance. + #[error("Account cannot be created for a held balance.")] + CannotCreateHold, + /// Withdrawal would cause unwanted loss of account. + #[error("Withdrawal would cause unwanted loss of account.")] + NotExpendable, +} + +/// An error relating to arithmetic when dispatching a transaction. +#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum ArithmeticError { + /// Underflow. + #[error("Underflow.")] + Underflow, + /// Overflow. + #[error("Overflow.")] + Overflow, + /// Division by zero. + #[error("Division by zero.")] + DivisionByZero, +} + +/// An error relating to thr transactional layers when dispatching a transaction. +#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum TransactionalError { + /// Too many transactional layers have been spawned. + #[error("Too many transactional layers have been spawned.")] + LimitReached, + /// A transactional layer was expected, but does not exist. + #[error("A transactional layer was expected, but does not exist.")] + NoLayer, +} + +/// Details about a module error that has occurred. +#[derive(Clone, Debug, thiserror::Error)] +#[non_exhaustive] +pub struct ModuleError { + metadata: Metadata, + raw: RawModuleError, +} + +impl PartialEq for ModuleError { + fn eq(&self, other: &Self) -> bool { + // A module error is the same if the raw underlying details are the same. + self.raw == other.raw + } +} +impl Eq for ModuleError {} + +impl std::fmt::Display for ModuleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Ok(details) = self.details() else { + return f.write_str("Unknown pallet error (pallet and error details cannot be retrieved)") + }; + + let pallet = details.pallet(); + let error = details.error(); + write!(f, "Pallet error {pallet}::{error}") + } +} + +impl ModuleError { + /// Return more details about this error. + pub fn details(&self) -> Result<&crate::metadata::ErrorMetadata, super::Error> { + let error_details = self + .metadata + .error(self.raw.pallet_index, self.raw.error[0])?; + Ok(error_details) + } + /// Return the underlying module error data that was decoded. + pub fn raw(&self) -> RawModuleError { + self.raw + } +} + +/// The error details about a module error that has occurred. +/// +/// **Note**: Structure used to obtain the underlying bytes of a ModuleError. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RawModuleError { + /// Index of the pallet that the error came from. + pub pallet_index: u8, + /// Raw error bytes. + pub error: [u8; 4], +} + +impl RawModuleError { + /// Obtain the error index from the underlying byte data. + pub fn error_index(&self) -> u8 { + // Error index is utilized as the first byte from the error array. + self.error[0] + } +} + +impl DispatchError { + /// Attempt to decode a runtime [`DispatchError`]. + #[doc(hidden)] + pub fn decode_from<'a>( + bytes: impl Into>, + metadata: Metadata, + ) -> Result { + let bytes = bytes.into(); + + let dispatch_error_ty_id = match metadata.dispatch_error_ty() { + Some(id) => id, + None => { + tracing::warn!( + "Can't decode error: sp_runtime::DispatchError was not found in Metadata" + ); + return Err(super::Error::Unknown(bytes.into_owned())); + } + }; + + // The aim is to decode our bytes into roughly this shape. This is copied from + // `sp_runtime::DispatchError`; we need the variant names and any inner variant + // names/shapes to line up in order for decoding to be successful. + #[derive(scale_decode::DecodeAsType)] + enum DecodedDispatchError { + Other, + CannotLookup, + BadOrigin, + Module(DecodedModuleErrorBytes), + ConsumerRemaining, + NoProviders, + TooManyConsumers, + Token(TokenError), + Arithmetic(ArithmeticError), + Transactional(TransactionalError), + Exhausted, + Corruption, + Unavailable, + } + + // ModuleError is a bit special; we want to support being decoded from either + // a legacy format of 2 bytes, or a newer format of 5 bytes. So, just grab the bytes + // out when decoding to manually work with them. + struct DecodedModuleErrorBytes(Vec); + struct DecodedModuleErrorBytesVisitor; + impl scale_decode::Visitor for DecodedModuleErrorBytesVisitor { + type Error = scale_decode::Error; + type Value<'scale, 'info> = DecodedModuleErrorBytes; + 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(Ok(DecodedModuleErrorBytes(input.to_vec()))) + } + } + impl scale_decode::IntoVisitor for DecodedModuleErrorBytes { + type Visitor = DecodedModuleErrorBytesVisitor; + fn into_visitor() -> Self::Visitor { + DecodedModuleErrorBytesVisitor + } + } + + // Decode into our temporary error: + let decoded_dispatch_err = DecodedDispatchError::decode_with_metadata( + &mut &*bytes, + dispatch_error_ty_id, + &metadata, + )?; + + // Convert into the outward-facing error, mainly by handling the Module variant. + let dispatch_error = match decoded_dispatch_err { + // Mostly we don't change anything from our decoded to our outward-facing error: + DecodedDispatchError::Other => DispatchError::Other, + DecodedDispatchError::CannotLookup => DispatchError::CannotLookup, + DecodedDispatchError::BadOrigin => DispatchError::BadOrigin, + DecodedDispatchError::ConsumerRemaining => DispatchError::ConsumerRemaining, + DecodedDispatchError::NoProviders => DispatchError::NoProviders, + DecodedDispatchError::TooManyConsumers => DispatchError::TooManyConsumers, + DecodedDispatchError::Token(val) => DispatchError::Token(val), + DecodedDispatchError::Arithmetic(val) => DispatchError::Arithmetic(val), + DecodedDispatchError::Transactional(val) => DispatchError::Transactional(val), + DecodedDispatchError::Exhausted => DispatchError::Exhausted, + DecodedDispatchError::Corruption => DispatchError::Corruption, + DecodedDispatchError::Unavailable => DispatchError::Unavailable, + // But we apply custom logic to transform the module error into the outward facing version: + DecodedDispatchError::Module(module_bytes) => { + let module_bytes = module_bytes.0; + + // The old version is 2 bytes; a pallet and error index. + // The new version is 5 bytes; a pallet and error index and then 3 extra bytes. + let raw = if module_bytes.len() == 2 { + RawModuleError { + pallet_index: module_bytes[0], + error: [module_bytes[1], 0, 0, 0], + } + } else if module_bytes.len() == 5 { + RawModuleError { + pallet_index: module_bytes[0], + error: [ + module_bytes[1], + module_bytes[2], + module_bytes[3], + module_bytes[4], + ], + } + } else { + tracing::warn!("Can't decode error sp_runtime::DispatchError: bytes do not match known shapes"); + // Return _all_ of the bytes; every "unknown" return should be consistent. + return Err(super::Error::Unknown(bytes.to_vec())); + }; + + // And return our outward-facing version: + DispatchError::Module(ModuleError { metadata, raw }) + } + }; + + Ok(dispatch_error) + } +} diff --git a/subxt/src/error/mod.rs b/subxt/src/error/mod.rs new file mode 100644 index 0000000000..787393eb6b --- /dev/null +++ b/subxt/src/error/mod.rs @@ -0,0 +1,155 @@ +// Copyright 2019-2022 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! Types representing the errors that can be returned. + +mod dispatch_error; + +use core::fmt::Debug; + +// Re-export dispatch error types: +pub use dispatch_error::{ + ArithmeticError, DispatchError, ModuleError, RawModuleError, TokenError, TransactionalError, +}; + +// Re-expose the errors we use from other crates here: +pub use crate::metadata::{InvalidMetadataError, MetadataError}; +pub use scale_decode::Error as DecodeError; +pub use scale_encode::Error as EncodeError; + +/// The underlying error enum, generic over the type held by the `Runtime` +/// variant. Prefer to use the [`Error`] and [`Error`] aliases over +/// using this type directly. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// Io error. + #[error("Io error: {0}")] + Io(#[from] std::io::Error), + /// Codec error. + #[error("Scale codec error: {0}")] + Codec(#[from] codec::Error), + /// Rpc error. + #[error("Rpc error: {0}")] + Rpc(#[from] RpcError), + /// Serde serialization error + #[error("Serde json error: {0}")] + Serialization(#[from] serde_json::error::Error), + /// Invalid metadata error + #[error("Invalid Metadata: {0}")] + InvalidMetadata(#[from] InvalidMetadataError), + /// Invalid metadata error + #[error("Metadata: {0}")] + Metadata(#[from] MetadataError), + /// Runtime error. + #[error("Runtime error: {0:?}")] + Runtime(#[from] DispatchError), + /// Error decoding to a [`crate::dynamic::Value`]. + #[error("Error decoding into dynamic value: {0}")] + Decode(#[from] DecodeError), + /// Error encoding from a [`crate::dynamic::Value`]. + #[error("Error encoding from dynamic value: {0}")] + Encode(#[from] EncodeError), + /// Transaction progress error. + #[error("Transaction error: {0}")] + Transaction(#[from] TransactionError), + /// Block related error. + #[error("Block error: {0}")] + Block(#[from] BlockError), + /// An error encoding a storage address. + #[error("Error encoding storage address: {0}")] + StorageAddress(#[from] StorageAddressError), + /// The bytes representing an error that we were unable to decode. + #[error("An error occurred but it could not be decoded: {0:?}")] + Unknown(Vec), + /// Other error. + #[error("Other error: {0}")] + Other(String), +} + +impl<'a> From<&'a str> for Error { + fn from(error: &'a str) -> Self { + Error::Other(error.into()) + } +} + +impl From for Error { + fn from(error: String) -> Self { + Error::Other(error) + } +} + +/// An RPC error. Since we are generic over the RPC client that is used, +/// the error is boxed and could be casted. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum RpcError { + // Dev note: We need the error to be safely sent between threads + // for `subscribe_to_block_headers_filling_in_gaps` and friends. + /// Error related to the RPC client. + #[error("RPC error: {0}")] + ClientError(Box), + /// The RPC subscription dropped. + #[error("RPC error: subscription dropped.")] + SubscriptionDropped, +} + +/// Block error +#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)] +#[non_exhaustive] +pub enum BlockError { + /// An error containing the hash of the block that was not found. + #[error("Could not find a block with hash {0} (perhaps it was on a non-finalized fork?)")] + NotFound(String), +} + +impl BlockError { + /// Produce an error that a block with the given hash cannot be found. + pub fn not_found(hash: impl AsRef<[u8]>) -> BlockError { + let hash = format!("0x{}", hex::encode(hash)); + BlockError::NotFound(hash) + } +} + +/// Transaction error. +#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)] +#[non_exhaustive] +pub enum TransactionError { + /// The finality subscription expired (after ~512 blocks we give up if the + /// block hasn't yet been finalized). + #[error("The finality subscription expired")] + FinalitySubscriptionTimeout, + /// The block hash that the transaction was added to could not be found. + /// This is probably because the block was retracted before being finalized. + #[error("The block containing the transaction can no longer be found (perhaps it was on a non-finalized fork?)")] + BlockNotFound, +} + +/// Something went wrong trying to encode a storage address. +#[derive(Clone, Debug, thiserror::Error)] +#[non_exhaustive] +pub enum StorageAddressError { + /// Storage map type must be a composite type. + #[error("Storage map type must be a composite type")] + MapTypeMustBeTuple, + /// Storage lookup does not have the expected number of keys. + #[error("Storage lookup requires {expected} keys but got {actual} keys")] + WrongNumberOfKeys { + /// The actual number of keys needed, based on the metadata. + actual: usize, + /// The number of keys provided in the storage address. + expected: usize, + }, + /// Storage lookup requires a type that wasn't found in the metadata. + #[error("Storage lookup requires type {0} to exist in the metadata, but it was not found")] + TypeNotFound(u32), + /// This storage entry in the metadata does not have the correct number of hashers to fields. + #[error("Storage entry in metadata does not have the correct number of hashers to fields")] + WrongNumberOfHashers { + /// The number of hashers in the metadata for this storage entry. + hashers: usize, + /// The number of fields in the metadata for this storage entry. + fields: usize, + }, +} diff --git a/subxt/src/rpc/rpc.rs b/subxt/src/rpc/rpc.rs index 9dc79c226f..e30ba119bf 100644 --- a/subxt/src/rpc/rpc.rs +++ b/subxt/src/rpc/rpc.rs @@ -444,10 +444,10 @@ impl Rpc { &self, encoded_signed: &[u8], at: Option, - ) -> Result { + ) -> Result { let params = rpc_params![to_hex(encoded_signed), at]; let result_bytes: types::Bytes = self.client.request("system_dryRun", params).await?; - Ok(types::decode_dry_run_result(&mut &*result_bytes.0)?) + Ok(types::DryRunResultBytes(result_bytes.0)) } /// Subscribe to `chainHead_unstable_follow` to obtain all reported blocks by the chain. diff --git a/subxt/src/rpc/types.rs b/subxt/src/rpc/types.rs index a4ffacf352..e1c3933bc8 100644 --- a/subxt/src/rpc/types.rs +++ b/subxt/src/rpc/types.rs @@ -4,7 +4,7 @@ //! Types sent to/from the Substrate RPC interface. -use crate::Config; +use crate::{metadata::Metadata, Config}; use codec::{Decode, Encode}; use primitive_types::U256; use serde::{Deserialize, Serialize}; @@ -13,40 +13,42 @@ use std::collections::HashMap; // Subscription types are returned from some calls, so expose it with the rest of the returned types. pub use super::rpc_client::Subscription; -/// Signal what the result of doing a dry run of an extrinsic is. -pub type DryRunResult = Result<(), DryRunError>; - /// An error dry running an extrinsic. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum DryRunError { - /// The extrinsic will not be included in the block +#[derive(Debug, PartialEq, Eq)] +pub enum DryRunResult { + /// The transaction could be included in the block and executed. + Success, + /// The transaction could be included in the block, but the call failed to dispatch. + DispatchError(crate::error::DispatchError), + /// The transaction could not be included in the block. TransactionValidityError, - /// The extrinsic will be included in the block, but the call failed to dispatch. - DispatchError, } -/// dryRun returns an ApplyExtrinsicResult, which is basically a -/// `Result, TransactionValidityError>`. We want to convert this to -/// a [`DryRunResult`]. -/// -/// - if `Ok(inner)`, the transaction will be included in the block -/// - if `Ok(Ok(()))`, the transaction will be included and the call will be dispatched -/// successfully -/// - if `Ok(Err(e))`, the transaction will be included but there is some error dispatching -/// the call to the module. -/// -/// The errors get a bit involved and have been known to change over time. At the moment -/// then, we will keep things simple here and just decode the Result portion (ie the initial bytes) -/// and ignore the rest. -pub(crate) fn decode_dry_run_result( - input: &mut I, -) -> Result { - let res = match , ()>>::decode(input)? { - Ok(Ok(())) => Ok(()), - Ok(Err(())) => Err(DryRunError::DispatchError), - Err(()) => Err(DryRunError::TransactionValidityError), - }; - Ok(res) +/// The bytes representing an error dry running an extrinsic. +pub struct DryRunResultBytes(pub Vec); + +impl DryRunResultBytes { + /// Attempt to decode the error bytes into a [`DryRunResult`] using the provided [`Metadata`]. + pub fn into_dry_run_result(self, metadata: &Metadata) -> Result { + // dryRun returns an ApplyExtrinsicResult, which is basically a + // `Result, TransactionValidityError>`. + let bytes = self.0; + if bytes[0] == 0 && bytes[1] == 0 { + // Ok(Ok(())); transaction is valid and executed ok + Ok(DryRunResult::Success) + } else if bytes[0] == 0 && bytes[1] == 1 { + // Ok(Err(dispatch_error)); transaction is valid but execution failed + let dispatch_error = + crate::error::DispatchError::decode_from(&bytes[2..], metadata.clone())?; + Ok(DryRunResult::DispatchError(dispatch_error)) + } else if bytes[0] == 1 { + // Err(transaction_error); some transaction validity error (we ignore the details at the moment) + Ok(DryRunResult::TransactionValidityError) + } else { + // unable to decode the bytes; they aren't what we expect. + Err(crate::Error::Unknown(bytes)) + } + } } /// A number type that can be serialized both as a number or a string that encodes a number in a @@ -817,39 +819,6 @@ mod test { assert_deser(r#"1000000000000"#, NumberOrHex::Number(1000000000000)); } - #[test] - fn dry_run_result_is_substrate_compatible() { - use sp_runtime::{ - transaction_validity::{ - InvalidTransaction as SpInvalidTransaction, - TransactionValidityError as SpTransactionValidityError, - }, - ApplyExtrinsicResult as SpApplyExtrinsicResult, DispatchError as SpDispatchError, - }; - - let pairs = vec![ - // All ok - (SpApplyExtrinsicResult::Ok(Ok(())), Ok(())), - // Some transaction error - ( - SpApplyExtrinsicResult::Err(SpTransactionValidityError::Invalid( - SpInvalidTransaction::BadProof, - )), - Err(DryRunError::TransactionValidityError), - ), - // Some dispatch error - ( - SpApplyExtrinsicResult::Ok(Err(SpDispatchError::BadOrigin)), - Err(DryRunError::DispatchError), - ), - ]; - - for (actual, expected) in pairs { - let encoded = actual.encode(); - assert_eq!(decode_dry_run_result(&mut &*encoded).unwrap(), expected); - } - } - #[test] fn justification_is_substrate_compatible() { use sp_runtime::Justification as SpJustification; diff --git a/subxt/src/tx/tx_client.rs b/subxt/src/tx/tx_client.rs index de3ab4e406..0b5f3c08c1 100644 --- a/subxt/src/tx/tx_client.rs +++ b/subxt/src/tx/tx_client.rs @@ -462,6 +462,7 @@ where /// /// Returns `Ok` with a [`DryRunResult`], which is the result of attempting to dry run the extrinsic. pub async fn dry_run(&self, at: Option) -> Result { - self.client.rpc().dry_run(self.encoded(), at).await + let dry_run_bytes = self.client.rpc().dry_run(self.encoded(), at).await?; + dry_run_bytes.into_dry_run_result(&self.client.metadata()) } } diff --git a/subxt/src/tx/tx_progress.rs b/subxt/src/tx/tx_progress.rs index 2fa1af119d..b5266f5bdc 100644 --- a/subxt/src/tx/tx_progress.rs +++ b/subxt/src/tx/tx_progress.rs @@ -324,7 +324,7 @@ impl> TxInBlock { let ev = ev?; if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" { let dispatch_error = - DispatchError::decode_from(ev.field_bytes(), &self.client.metadata()); + DispatchError::decode_from(ev.field_bytes(), self.client.metadata())?; return Err(dispatch_error.into()); } } @@ -344,7 +344,7 @@ impl> TxInBlock { .rpc() .block(Some(self.block_hash)) .await? - .ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?; + .ok_or(Error::Transaction(TransactionError::BlockNotFound))?; let extrinsic_idx = block .block @@ -357,7 +357,7 @@ impl> TxInBlock { }) // If we successfully obtain the block hash we think contains our // extrinsic, the extrinsic should be in there somewhere.. - .ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?; + .ok_or(Error::Transaction(TransactionError::BlockNotFound))?; let events = EventsClient::new(self.client.clone()) .at(Some(self.block_hash)) diff --git a/testing/integration-tests/Cargo.toml b/testing/integration-tests/Cargo.toml index 77a0fb1750..8dfa662f16 100644 --- a/testing/integration-tests/Cargo.toml +++ b/testing/integration-tests/Cargo.toml @@ -24,6 +24,7 @@ hex = "0.4.3" regex = "1.5.0" scale-info = { version = "2.0.0", features = ["bit-vec"] } sp-core = { version = "18.0.0", default-features = false } +sp-runtime = "20.0.0" sp-keyring = "20.0.0" syn = "1.0.109" subxt = { version = "0.27.1", path = "../../subxt" } diff --git a/testing/integration-tests/src/client/mod.rs b/testing/integration-tests/src/client/mod.rs index 7076967481..0baf465a92 100644 --- a/testing/integration-tests/src/client/mod.rs +++ b/testing/integration-tests/src/client/mod.rs @@ -10,9 +10,14 @@ use assert_matches::assert_matches; use codec::{Compact, Decode, Encode}; use frame_metadata::RuntimeMetadataPrefixed; use sp_core::storage::well_known_keys; +use sp_core::{sr25519::Pair as Sr25519Pair, Pair}; use sp_keyring::AccountKeyring; use subxt::{ - rpc::types::{ChainHeadEvent, FollowEvent, Initialized, RuntimeEvent, RuntimeVersionEvent}, + error::{DispatchError, Error, TokenError}, + rpc::types::{ + ChainHeadEvent, DryRunResult, DryRunResultBytes, FollowEvent, Initialized, RuntimeEvent, + RuntimeVersionEvent, + }, tx::Signer, utils::AccountId32, }; @@ -169,8 +174,7 @@ async fn dry_run_passes() { signed_extrinsic .dry_run(None) .await - .expect("dryrunning failed") - .expect("dry run should be successful"); + .expect("dryrunning failed"); signed_extrinsic .submit_and_watch() @@ -181,49 +185,111 @@ async fn dry_run_passes() { .unwrap(); } -//// [jsdw] Commented out until Subxt decodes these new Token errors better -// #[tokio::test] -// async fn dry_run_fails() { -// let ctx = test_context().await; -// let api = ctx.client(); -// -// wait_for_blocks(&api).await; -// -// let alice = pair_signer(AccountKeyring::Alice.pair()); -// let hans = pair_signer(Sr25519Pair::generate().0); -// -// let tx = node_runtime::tx().balances().transfer( -// hans.account_id().clone().into(), -// 100_000_000_000_000_000_000_000_000_000_000_000, -// ); -// -// let signed_extrinsic = api -// .tx() -// .create_signed(&tx, &alice, Default::default()) -// .await -// .unwrap(); -// -// let dry_run_res = signed_extrinsic -// .dry_run(None) -// .await -// .expect("dryrunning failed"); -// -// assert_eq!(dry_run_res, Err(DryRunError::DispatchError)); -// -// let res = signed_extrinsic -// .submit_and_watch() -// .await -// .unwrap() -// .wait_for_finalized_success() -// .await; -// -// if let Err(subxt::error::Error::Runtime(DispatchError::Module(err))) = res { -// assert_eq!(err.pallet, "Balances"); -// assert_eq!(err.error, "InsufficientBalance"); -// } else { -// panic!("expected a runtime module error"); -// } -// } +#[tokio::test] +async fn dry_run_fails() { + let ctx = test_context().await; + let api = ctx.client(); + + wait_for_blocks(&api).await; + + let alice = pair_signer(AccountKeyring::Alice.pair()); + let hans = pair_signer(Sr25519Pair::generate().0); + + let tx = node_runtime::tx().balances().transfer( + hans.account_id().clone().into(), + // 7 more than the default amount Alice has, so this should fail; insufficient funds: + 1_000_000_000_000_000_000_007, + ); + + let signed_extrinsic = api + .tx() + .create_signed(&tx, &alice, Default::default()) + .await + .unwrap(); + + let dry_run_res = signed_extrinsic + .dry_run(None) + .await + .expect("dryrunning failed"); + + assert_eq!( + dry_run_res, + DryRunResult::DispatchError(DispatchError::Token(TokenError::FundsUnavailable)) + ); + + let res = signed_extrinsic + .submit_and_watch() + .await + .unwrap() + .wait_for_finalized_success() + .await; + + assert!( + matches!( + res, + Err(Error::Runtime(DispatchError::Token( + TokenError::FundsUnavailable + ))) + ), + "Expected an insufficient balance, got {res:?}" + ); +} + +#[tokio::test] +async fn dry_run_result_is_substrate_compatible() { + use sp_runtime::{ + transaction_validity::{ + InvalidTransaction as SpInvalidTransaction, + TransactionValidityError as SpTransactionValidityError, + }, + ApplyExtrinsicResult as SpApplyExtrinsicResult, DispatchError as SpDispatchError, + TokenError as SpTokenError, + }; + + // We really just connect to a node to get some valid metadata to help us + // decode Dispatch Errors. + let ctx = test_context().await; + let api = ctx.client(); + + let pairs = vec![ + // All ok + (SpApplyExtrinsicResult::Ok(Ok(())), DryRunResult::Success), + // Some transaction error + ( + SpApplyExtrinsicResult::Err(SpTransactionValidityError::Invalid( + SpInvalidTransaction::BadProof, + )), + DryRunResult::TransactionValidityError, + ), + // Some dispatch errors to check that they decode OK. We've tested module errors + // "in situ" in other places so avoid the complexity of testing them properly here. + ( + SpApplyExtrinsicResult::Ok(Err(SpDispatchError::Other("hi"))), + DryRunResult::DispatchError(DispatchError::Other), + ), + ( + SpApplyExtrinsicResult::Ok(Err(SpDispatchError::CannotLookup)), + DryRunResult::DispatchError(DispatchError::CannotLookup), + ), + ( + SpApplyExtrinsicResult::Ok(Err(SpDispatchError::BadOrigin)), + DryRunResult::DispatchError(DispatchError::BadOrigin), + ), + ( + SpApplyExtrinsicResult::Ok(Err(SpDispatchError::Token(SpTokenError::CannotCreate))), + DryRunResult::DispatchError(DispatchError::Token(TokenError::CannotCreate)), + ), + ]; + + for (actual, expected) in pairs { + let encoded = actual.encode(); + let res = DryRunResultBytes(encoded) + .into_dry_run_result(&api.metadata()) + .unwrap(); + + assert_eq!(res, expected); + } +} #[tokio::test] async fn external_signing() { diff --git a/testing/integration-tests/src/frame/balances.rs b/testing/integration-tests/src/frame/balances.rs index b1877283be..747f964437 100644 --- a/testing/integration-tests/src/frame/balances.rs +++ b/testing/integration-tests/src/frame/balances.rs @@ -7,8 +7,12 @@ use crate::{ pair_signer, test_context, }; use codec::Decode; +use sp_core::Pair; use sp_keyring::AccountKeyring; -use subxt::utils::{AccountId32, MultiAddress}; +use subxt::{ + error::{DispatchError, Error, TokenError}, + utils::{AccountId32, MultiAddress}, +}; #[tokio::test] async fn tx_basic_transfer() -> Result<(), subxt::Error> { @@ -296,46 +300,48 @@ async fn storage_balance_lock() -> Result<(), subxt::Error> { Ok(()) } -//// [jsdw] Commented out until Subxt decodes these new Token errors better -// #[tokio::test] -// async fn transfer_error() { -// let alice = pair_signer(AccountKeyring::Alice.pair()); -// let alice_addr = alice.account_id().clone().into(); -// let hans = pair_signer(Pair::generate().0); -// let hans_address = hans.account_id().clone().into(); -// let ctx = test_context().await; -// let api = ctx.client(); -// -// let to_hans_tx = node_runtime::tx() -// .balances() -// .transfer(hans_address, 100_000_000_000_000_000); -// let to_alice_tx = node_runtime::tx() -// .balances() -// .transfer(alice_addr, 100_000_000_000_000_000); -// -// api.tx() -// .sign_and_submit_then_watch_default(&to_hans_tx, &alice) -// .await -// .unwrap() -// .wait_for_finalized_success() -// .await -// .unwrap(); -// -// let res = api -// .tx() -// .sign_and_submit_then_watch_default(&to_alice_tx, &hans) -// .await -// .unwrap() -// .wait_for_finalized_success() -// .await; -// -// if let Err(Error::Runtime(DispatchError::Module(err))) = res { -// assert_eq!(err.pallet, "Balances"); -// assert_eq!(err.error, "InsufficientBalance"); -// } else { -// panic!("expected a runtime module error"); -// } -// } +#[tokio::test] +async fn transfer_error() { + let alice = pair_signer(AccountKeyring::Alice.pair()); + let alice_addr = alice.account_id().clone().into(); + let hans = pair_signer(Pair::generate().0); + let hans_address = hans.account_id().clone().into(); + let ctx = test_context().await; + let api = ctx.client(); + + let to_hans_tx = node_runtime::tx() + .balances() + .transfer(hans_address, 100_000_000_000_000_000); + let to_alice_tx = node_runtime::tx() + .balances() + .transfer(alice_addr, 100_000_000_000_000_000); + + api.tx() + .sign_and_submit_then_watch_default(&to_hans_tx, &alice) + .await + .unwrap() + .wait_for_finalized_success() + .await + .unwrap(); + + let res = api + .tx() + .sign_and_submit_then_watch_default(&to_alice_tx, &hans) + .await + .unwrap() + .wait_for_finalized_success() + .await; + + assert!( + matches!( + res, + Err(Error::Runtime(DispatchError::Token( + TokenError::FundsUnavailable + ))) + ), + "Expected an insufficient balance, got {res:?}" + ); +} #[tokio::test] async fn transfer_implicit_subscription() { diff --git a/testing/integration-tests/src/frame/staking.rs b/testing/integration-tests/src/frame/staking.rs index b722f2733a..fa198c4366 100644 --- a/testing/integration-tests/src/frame/staking.rs +++ b/testing/integration-tests/src/frame/staking.rs @@ -68,8 +68,9 @@ async fn validate_not_possible_for_stash_account() -> Result<(), Error> { .wait_for_finalized_success() .await; assert_matches!(announce_validator, Err(Error::Runtime(DispatchError::Module(err))) => { - assert_eq!(err.pallet, "Staking"); - assert_eq!(err.error, "NotController"); + let details = err.details().unwrap(); + assert_eq!(details.pallet(), "Staking"); + assert_eq!(details.error(), "NotController"); }); Ok(()) } @@ -116,8 +117,9 @@ async fn nominate_not_possible_for_stash_account() -> Result<(), Error> { .await; assert_matches!(nomination, Err(Error::Runtime(DispatchError::Module(err))) => { - assert_eq!(err.pallet, "Staking"); - assert_eq!(err.error, "NotController"); + let details = err.details().unwrap(); + assert_eq!(details.pallet(), "Staking"); + assert_eq!(details.error(), "NotController"); }); Ok(()) } @@ -161,8 +163,9 @@ async fn chill_works_for_controller_only() -> Result<(), Error> { .await; assert_matches!(chill, Err(Error::Runtime(DispatchError::Module(err))) => { - assert_eq!(err.pallet, "Staking"); - assert_eq!(err.error, "NotController"); + let details = err.details().unwrap(); + assert_eq!(details.pallet(), "Staking"); + assert_eq!(details.error(), "NotController"); }); let is_chilled = api @@ -207,8 +210,9 @@ async fn tx_bond() -> Result<(), Error> { .await; assert_matches!(bond_again, Err(Error::Runtime(DispatchError::Module(err))) => { - assert_eq!(err.pallet, "Staking"); - assert_eq!(err.error, "AlreadyBonded"); + let details = err.details().unwrap(); + assert_eq!(details.pallet(), "Staking"); + assert_eq!(details.error(), "AlreadyBonded"); }); Ok(()) }