mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-23 04:55:41 +00:00
Improve Dispatch Errors (#878)
* better dispatch errors * dry_run to use same DispatchError * fix dry_run_fails; use correct transfer amount * Hide ModuleError impl and avoid pulling details from metadata unless user needs them * fix tests * actually fix the tests (hopefully..) * Add a couple more DispatchError test cases * Add a comment about where the error was copied from * Also expose a way to obtain the raw module error data * Remove redundant variant prefixes * explicit lifetime on From<str> for clarity * fmt
This commit is contained in:
Generated
+1
@@ -1545,6 +1545,7 @@ dependencies = [
|
|||||||
"scale-info",
|
"scale-info",
|
||||||
"sp-core",
|
"sp-core",
|
||||||
"sp-keyring",
|
"sp-keyring",
|
||||||
|
"sp-runtime",
|
||||||
"subxt",
|
"subxt",
|
||||||
"subxt-codegen",
|
"subxt-codegen",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ where
|
|||||||
let block_hash = self.header.hash();
|
let block_hash = self.header.hash();
|
||||||
let block_details = match self.client.rpc().block(Some(block_hash)).await? {
|
let block_details = match self.client.rpc().block(Some(block_hash)).await? {
|
||||||
Some(block) => block,
|
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(
|
Ok(BlockBody::new(
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ where
|
|||||||
|
|
||||||
let block_header = match client.rpc().header(Some(block_hash)).await? {
|
let block_header = match client.rpc().header(Some(block_hash)).await? {
|
||||||
Some(header) => header,
|
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))
|
Ok(Block::new(block_header, client))
|
||||||
|
|||||||
@@ -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<E>`] 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<String> for Error {
|
|
||||||
fn from(error: String) -> Self {
|
|
||||||
Error::Other(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DispatchError> 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<dyn std::error::Error + Send + Sync + 'static>),
|
|
||||||
/// 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<u8>),
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Cow<'a, [u8]>>, 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<String>,
|
|
||||||
/// 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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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<Cow<'a, [u8]>>,
|
||||||
|
metadata: Metadata,
|
||||||
|
) -> Result<Self, super::Error> {
|
||||||
|
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<u8>);
|
||||||
|
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, Result<Self::Value<'scale, 'info>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<E>`] 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<u8>),
|
||||||
|
/// 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<String> 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<dyn std::error::Error + Send + Sync + 'static>),
|
||||||
|
/// 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -444,10 +444,10 @@ impl<T: Config> Rpc<T> {
|
|||||||
&self,
|
&self,
|
||||||
encoded_signed: &[u8],
|
encoded_signed: &[u8],
|
||||||
at: Option<T::Hash>,
|
at: Option<T::Hash>,
|
||||||
) -> Result<types::DryRunResult, Error> {
|
) -> Result<types::DryRunResultBytes, Error> {
|
||||||
let params = rpc_params![to_hex(encoded_signed), at];
|
let params = rpc_params![to_hex(encoded_signed), at];
|
||||||
let result_bytes: types::Bytes = self.client.request("system_dryRun", params).await?;
|
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.
|
/// Subscribe to `chainHead_unstable_follow` to obtain all reported blocks by the chain.
|
||||||
|
|||||||
+33
-64
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
//! Types sent to/from the Substrate RPC interface.
|
//! Types sent to/from the Substrate RPC interface.
|
||||||
|
|
||||||
use crate::Config;
|
use crate::{metadata::Metadata, Config};
|
||||||
use codec::{Decode, Encode};
|
use codec::{Decode, Encode};
|
||||||
use primitive_types::U256;
|
use primitive_types::U256;
|
||||||
use serde::{Deserialize, Serialize};
|
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.
|
// Subscription types are returned from some calls, so expose it with the rest of the returned types.
|
||||||
pub use super::rpc_client::Subscription;
|
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.
|
/// An error dry running an extrinsic.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum DryRunError {
|
pub enum DryRunResult {
|
||||||
/// The extrinsic will not be included in the block
|
/// 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,
|
TransactionValidityError,
|
||||||
/// The extrinsic will be included in the block, but the call failed to dispatch.
|
|
||||||
DispatchError,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// dryRun returns an ApplyExtrinsicResult, which is basically a
|
/// The bytes representing an error dry running an extrinsic.
|
||||||
/// `Result<Result<(), DispatchError>, TransactionValidityError>`. We want to convert this to
|
pub struct DryRunResultBytes(pub Vec<u8>);
|
||||||
/// a [`DryRunResult`].
|
|
||||||
///
|
impl DryRunResultBytes {
|
||||||
/// - if `Ok(inner)`, the transaction will be included in the block
|
/// Attempt to decode the error bytes into a [`DryRunResult`] using the provided [`Metadata`].
|
||||||
/// - if `Ok(Ok(()))`, the transaction will be included and the call will be dispatched
|
pub fn into_dry_run_result(self, metadata: &Metadata) -> Result<DryRunResult, crate::Error> {
|
||||||
/// successfully
|
// dryRun returns an ApplyExtrinsicResult, which is basically a
|
||||||
/// - if `Ok(Err(e))`, the transaction will be included but there is some error dispatching
|
// `Result<Result<(), DispatchError>, TransactionValidityError>`.
|
||||||
/// the call to the module.
|
let bytes = self.0;
|
||||||
///
|
if bytes[0] == 0 && bytes[1] == 0 {
|
||||||
/// The errors get a bit involved and have been known to change over time. At the moment
|
// Ok(Ok(())); transaction is valid and executed ok
|
||||||
/// then, we will keep things simple here and just decode the Result portion (ie the initial bytes)
|
Ok(DryRunResult::Success)
|
||||||
/// and ignore the rest.
|
} else if bytes[0] == 0 && bytes[1] == 1 {
|
||||||
pub(crate) fn decode_dry_run_result<I: codec::Input>(
|
// Ok(Err(dispatch_error)); transaction is valid but execution failed
|
||||||
input: &mut I,
|
let dispatch_error =
|
||||||
) -> Result<DryRunResult, codec::Error> {
|
crate::error::DispatchError::decode_from(&bytes[2..], metadata.clone())?;
|
||||||
let res = match <Result<Result<(), ()>, ()>>::decode(input)? {
|
Ok(DryRunResult::DispatchError(dispatch_error))
|
||||||
Ok(Ok(())) => Ok(()),
|
} else if bytes[0] == 1 {
|
||||||
Ok(Err(())) => Err(DryRunError::DispatchError),
|
// Err(transaction_error); some transaction validity error (we ignore the details at the moment)
|
||||||
Err(()) => Err(DryRunError::TransactionValidityError),
|
Ok(DryRunResult::TransactionValidityError)
|
||||||
};
|
} else {
|
||||||
Ok(res)
|
// 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
|
/// 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));
|
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]
|
#[test]
|
||||||
fn justification_is_substrate_compatible() {
|
fn justification_is_substrate_compatible() {
|
||||||
use sp_runtime::Justification as SpJustification;
|
use sp_runtime::Justification as SpJustification;
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ where
|
|||||||
///
|
///
|
||||||
/// Returns `Ok` with a [`DryRunResult`], which is the result of attempting to dry run the extrinsic.
|
/// Returns `Ok` with a [`DryRunResult`], which is the result of attempting to dry run the extrinsic.
|
||||||
pub async fn dry_run(&self, at: Option<T::Hash>) -> Result<DryRunResult, Error> {
|
pub async fn dry_run(&self, at: Option<T::Hash>) -> Result<DryRunResult, Error> {
|
||||||
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
|||||||
let ev = ev?;
|
let ev = ev?;
|
||||||
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
|
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
|
||||||
let dispatch_error =
|
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());
|
return Err(dispatch_error.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +344,7 @@ impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
|||||||
.rpc()
|
.rpc()
|
||||||
.block(Some(self.block_hash))
|
.block(Some(self.block_hash))
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?;
|
.ok_or(Error::Transaction(TransactionError::BlockNotFound))?;
|
||||||
|
|
||||||
let extrinsic_idx = block
|
let extrinsic_idx = block
|
||||||
.block
|
.block
|
||||||
@@ -357,7 +357,7 @@ impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
|||||||
})
|
})
|
||||||
// If we successfully obtain the block hash we think contains our
|
// If we successfully obtain the block hash we think contains our
|
||||||
// extrinsic, the extrinsic should be in there somewhere..
|
// 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())
|
let events = EventsClient::new(self.client.clone())
|
||||||
.at(Some(self.block_hash))
|
.at(Some(self.block_hash))
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ hex = "0.4.3"
|
|||||||
regex = "1.5.0"
|
regex = "1.5.0"
|
||||||
scale-info = { version = "2.0.0", features = ["bit-vec"] }
|
scale-info = { version = "2.0.0", features = ["bit-vec"] }
|
||||||
sp-core = { version = "18.0.0", default-features = false }
|
sp-core = { version = "18.0.0", default-features = false }
|
||||||
|
sp-runtime = "20.0.0"
|
||||||
sp-keyring = "20.0.0"
|
sp-keyring = "20.0.0"
|
||||||
syn = "1.0.109"
|
syn = "1.0.109"
|
||||||
subxt = { version = "0.27.1", path = "../../subxt" }
|
subxt = { version = "0.27.1", path = "../../subxt" }
|
||||||
|
|||||||
@@ -10,9 +10,14 @@ use assert_matches::assert_matches;
|
|||||||
use codec::{Compact, Decode, Encode};
|
use codec::{Compact, Decode, Encode};
|
||||||
use frame_metadata::RuntimeMetadataPrefixed;
|
use frame_metadata::RuntimeMetadataPrefixed;
|
||||||
use sp_core::storage::well_known_keys;
|
use sp_core::storage::well_known_keys;
|
||||||
|
use sp_core::{sr25519::Pair as Sr25519Pair, Pair};
|
||||||
use sp_keyring::AccountKeyring;
|
use sp_keyring::AccountKeyring;
|
||||||
use subxt::{
|
use subxt::{
|
||||||
rpc::types::{ChainHeadEvent, FollowEvent, Initialized, RuntimeEvent, RuntimeVersionEvent},
|
error::{DispatchError, Error, TokenError},
|
||||||
|
rpc::types::{
|
||||||
|
ChainHeadEvent, DryRunResult, DryRunResultBytes, FollowEvent, Initialized, RuntimeEvent,
|
||||||
|
RuntimeVersionEvent,
|
||||||
|
},
|
||||||
tx::Signer,
|
tx::Signer,
|
||||||
utils::AccountId32,
|
utils::AccountId32,
|
||||||
};
|
};
|
||||||
@@ -169,8 +174,7 @@ async fn dry_run_passes() {
|
|||||||
signed_extrinsic
|
signed_extrinsic
|
||||||
.dry_run(None)
|
.dry_run(None)
|
||||||
.await
|
.await
|
||||||
.expect("dryrunning failed")
|
.expect("dryrunning failed");
|
||||||
.expect("dry run should be successful");
|
|
||||||
|
|
||||||
signed_extrinsic
|
signed_extrinsic
|
||||||
.submit_and_watch()
|
.submit_and_watch()
|
||||||
@@ -181,49 +185,111 @@ async fn dry_run_passes() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
//// [jsdw] Commented out until Subxt decodes these new Token errors better
|
#[tokio::test]
|
||||||
// #[tokio::test]
|
async fn dry_run_fails() {
|
||||||
// async fn dry_run_fails() {
|
let ctx = test_context().await;
|
||||||
// let ctx = test_context().await;
|
let api = ctx.client();
|
||||||
// let api = ctx.client();
|
|
||||||
//
|
wait_for_blocks(&api).await;
|
||||||
// wait_for_blocks(&api).await;
|
|
||||||
//
|
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||||
// let alice = pair_signer(AccountKeyring::Alice.pair());
|
let hans = pair_signer(Sr25519Pair::generate().0);
|
||||||
// let hans = pair_signer(Sr25519Pair::generate().0);
|
|
||||||
//
|
let tx = node_runtime::tx().balances().transfer(
|
||||||
// let tx = node_runtime::tx().balances().transfer(
|
hans.account_id().clone().into(),
|
||||||
// hans.account_id().clone().into(),
|
// 7 more than the default amount Alice has, so this should fail; insufficient funds:
|
||||||
// 100_000_000_000_000_000_000_000_000_000_000_000,
|
1_000_000_000_000_000_000_007,
|
||||||
// );
|
);
|
||||||
//
|
|
||||||
// let signed_extrinsic = api
|
let signed_extrinsic = api
|
||||||
// .tx()
|
.tx()
|
||||||
// .create_signed(&tx, &alice, Default::default())
|
.create_signed(&tx, &alice, Default::default())
|
||||||
// .await
|
.await
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
//
|
|
||||||
// let dry_run_res = signed_extrinsic
|
let dry_run_res = signed_extrinsic
|
||||||
// .dry_run(None)
|
.dry_run(None)
|
||||||
// .await
|
.await
|
||||||
// .expect("dryrunning failed");
|
.expect("dryrunning failed");
|
||||||
//
|
|
||||||
// assert_eq!(dry_run_res, Err(DryRunError::DispatchError));
|
assert_eq!(
|
||||||
//
|
dry_run_res,
|
||||||
// let res = signed_extrinsic
|
DryRunResult::DispatchError(DispatchError::Token(TokenError::FundsUnavailable))
|
||||||
// .submit_and_watch()
|
);
|
||||||
// .await
|
|
||||||
// .unwrap()
|
let res = signed_extrinsic
|
||||||
// .wait_for_finalized_success()
|
.submit_and_watch()
|
||||||
// .await;
|
.await
|
||||||
//
|
.unwrap()
|
||||||
// if let Err(subxt::error::Error::Runtime(DispatchError::Module(err))) = res {
|
.wait_for_finalized_success()
|
||||||
// assert_eq!(err.pallet, "Balances");
|
.await;
|
||||||
// assert_eq!(err.error, "InsufficientBalance");
|
|
||||||
// } else {
|
assert!(
|
||||||
// panic!("expected a runtime module error");
|
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]
|
#[tokio::test]
|
||||||
async fn external_signing() {
|
async fn external_signing() {
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ use crate::{
|
|||||||
pair_signer, test_context,
|
pair_signer, test_context,
|
||||||
};
|
};
|
||||||
use codec::Decode;
|
use codec::Decode;
|
||||||
|
use sp_core::Pair;
|
||||||
use sp_keyring::AccountKeyring;
|
use sp_keyring::AccountKeyring;
|
||||||
use subxt::utils::{AccountId32, MultiAddress};
|
use subxt::{
|
||||||
|
error::{DispatchError, Error, TokenError},
|
||||||
|
utils::{AccountId32, MultiAddress},
|
||||||
|
};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn tx_basic_transfer() -> Result<(), subxt::Error> {
|
async fn tx_basic_transfer() -> Result<(), subxt::Error> {
|
||||||
@@ -296,46 +300,48 @@ async fn storage_balance_lock() -> Result<(), subxt::Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
//// [jsdw] Commented out until Subxt decodes these new Token errors better
|
#[tokio::test]
|
||||||
// #[tokio::test]
|
async fn transfer_error() {
|
||||||
// async fn transfer_error() {
|
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||||
// let alice = pair_signer(AccountKeyring::Alice.pair());
|
let alice_addr = alice.account_id().clone().into();
|
||||||
// let alice_addr = alice.account_id().clone().into();
|
let hans = pair_signer(Pair::generate().0);
|
||||||
// let hans = pair_signer(Pair::generate().0);
|
let hans_address = hans.account_id().clone().into();
|
||||||
// let hans_address = hans.account_id().clone().into();
|
let ctx = test_context().await;
|
||||||
// let ctx = test_context().await;
|
let api = ctx.client();
|
||||||
// let api = ctx.client();
|
|
||||||
//
|
let to_hans_tx = node_runtime::tx()
|
||||||
// let to_hans_tx = node_runtime::tx()
|
.balances()
|
||||||
// .balances()
|
.transfer(hans_address, 100_000_000_000_000_000);
|
||||||
// .transfer(hans_address, 100_000_000_000_000_000);
|
let to_alice_tx = node_runtime::tx()
|
||||||
// let to_alice_tx = node_runtime::tx()
|
.balances()
|
||||||
// .balances()
|
.transfer(alice_addr, 100_000_000_000_000_000);
|
||||||
// .transfer(alice_addr, 100_000_000_000_000_000);
|
|
||||||
//
|
api.tx()
|
||||||
// api.tx()
|
.sign_and_submit_then_watch_default(&to_hans_tx, &alice)
|
||||||
// .sign_and_submit_then_watch_default(&to_hans_tx, &alice)
|
.await
|
||||||
// .await
|
.unwrap()
|
||||||
// .unwrap()
|
.wait_for_finalized_success()
|
||||||
// .wait_for_finalized_success()
|
.await
|
||||||
// .await
|
.unwrap();
|
||||||
// .unwrap();
|
|
||||||
//
|
let res = api
|
||||||
// let res = api
|
.tx()
|
||||||
// .tx()
|
.sign_and_submit_then_watch_default(&to_alice_tx, &hans)
|
||||||
// .sign_and_submit_then_watch_default(&to_alice_tx, &hans)
|
.await
|
||||||
// .await
|
.unwrap()
|
||||||
// .unwrap()
|
.wait_for_finalized_success()
|
||||||
// .wait_for_finalized_success()
|
.await;
|
||||||
// .await;
|
|
||||||
//
|
assert!(
|
||||||
// if let Err(Error::Runtime(DispatchError::Module(err))) = res {
|
matches!(
|
||||||
// assert_eq!(err.pallet, "Balances");
|
res,
|
||||||
// assert_eq!(err.error, "InsufficientBalance");
|
Err(Error::Runtime(DispatchError::Token(
|
||||||
// } else {
|
TokenError::FundsUnavailable
|
||||||
// panic!("expected a runtime module error");
|
)))
|
||||||
// }
|
),
|
||||||
// }
|
"Expected an insufficient balance, got {res:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn transfer_implicit_subscription() {
|
async fn transfer_implicit_subscription() {
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ async fn validate_not_possible_for_stash_account() -> Result<(), Error> {
|
|||||||
.wait_for_finalized_success()
|
.wait_for_finalized_success()
|
||||||
.await;
|
.await;
|
||||||
assert_matches!(announce_validator, Err(Error::Runtime(DispatchError::Module(err))) => {
|
assert_matches!(announce_validator, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||||
assert_eq!(err.pallet, "Staking");
|
let details = err.details().unwrap();
|
||||||
assert_eq!(err.error, "NotController");
|
assert_eq!(details.pallet(), "Staking");
|
||||||
|
assert_eq!(details.error(), "NotController");
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -116,8 +117,9 @@ async fn nominate_not_possible_for_stash_account() -> Result<(), Error> {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_matches!(nomination, Err(Error::Runtime(DispatchError::Module(err))) => {
|
assert_matches!(nomination, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||||
assert_eq!(err.pallet, "Staking");
|
let details = err.details().unwrap();
|
||||||
assert_eq!(err.error, "NotController");
|
assert_eq!(details.pallet(), "Staking");
|
||||||
|
assert_eq!(details.error(), "NotController");
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -161,8 +163,9 @@ async fn chill_works_for_controller_only() -> Result<(), Error> {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_matches!(chill, Err(Error::Runtime(DispatchError::Module(err))) => {
|
assert_matches!(chill, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||||
assert_eq!(err.pallet, "Staking");
|
let details = err.details().unwrap();
|
||||||
assert_eq!(err.error, "NotController");
|
assert_eq!(details.pallet(), "Staking");
|
||||||
|
assert_eq!(details.error(), "NotController");
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_chilled = api
|
let is_chilled = api
|
||||||
@@ -207,8 +210,9 @@ async fn tx_bond() -> Result<(), Error> {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_matches!(bond_again, Err(Error::Runtime(DispatchError::Module(err))) => {
|
assert_matches!(bond_again, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||||
assert_eq!(err.pallet, "Staking");
|
let details = err.details().unwrap();
|
||||||
assert_eq!(err.error, "AlreadyBonded");
|
assert_eq!(details.pallet(), "Staking");
|
||||||
|
assert_eq!(details.error(), "AlreadyBonded");
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user