pezkuwi_subxt/error/
dispatch_error.rs

1// Copyright 2019-2025 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5//! A representation of the dispatch error; an error returned when
6//! something fails in trying to submit/execute a transaction.
7
8use super::{DispatchErrorDecodeError, ModuleErrorDecodeError, ModuleErrorDetailsError};
9use crate::metadata::Metadata;
10use core::fmt::Debug;
11use scale_decode::{visitor::DecodeAsTypeResult, DecodeAsType, TypeResolver};
12use std::{borrow::Cow, marker::PhantomData};
13
14/// An error dispatching a transaction.
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16#[allow(clippy::large_enum_variant)]
17#[non_exhaustive]
18pub enum DispatchError {
19	/// Some error occurred.
20	#[error("Some unknown error occurred.")]
21	Other,
22	/// Failed to lookup some data.
23	#[error("Failed to lookup some data.")]
24	CannotLookup,
25	/// A bad origin.
26	#[error("Bad origin.")]
27	BadOrigin,
28	/// A custom error in a module.
29	#[error("Pallet error: {0}")]
30	Module(ModuleError),
31	/// At least one consumer is remaining so the account cannot be destroyed.
32	#[error("At least one consumer is remaining so the account cannot be destroyed.")]
33	ConsumerRemaining,
34	/// There are no providers so the account cannot be created.
35	#[error("There are no providers so the account cannot be created.")]
36	NoProviders,
37	/// There are too many consumers so the account cannot be created.
38	#[error("There are too many consumers so the account cannot be created.")]
39	TooManyConsumers,
40	/// An error to do with tokens.
41	#[error("Token error: {0}")]
42	Token(TokenError),
43	/// An arithmetic error.
44	#[error("Arithmetic error: {0}")]
45	Arithmetic(ArithmeticError),
46	/// The number of transactional layers has been reached, or we are not in a transactional layer.
47	#[error("Transactional error: {0}")]
48	Transactional(TransactionalError),
49	/// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate.
50	#[error(
51		"Resources exhausted, e.g. attempt to read/write data which is too large to manipulate."
52	)]
53	Exhausted,
54	/// The state is corrupt; this is generally not going to fix itself.
55	#[error("The state is corrupt; this is generally not going to fix itself.")]
56	Corruption,
57	/// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later.
58	#[error(
59		"Some resource (e.g. a preimage) is unavailable right now. This might fix itself later."
60	)]
61	Unavailable,
62	/// Root origin is not allowed.
63	#[error("Root origin is not allowed.")]
64	RootNotAllowed,
65}
66
67/// An error relating to tokens when dispatching a transaction.
68#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
69#[non_exhaustive]
70pub enum TokenError {
71	/// Funds are unavailable.
72	#[error("Funds are unavailable.")]
73	FundsUnavailable,
74	/// Some part of the balance gives the only provider reference to the account and thus cannot be
75	/// (re)moved.
76	#[error(
77		"Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved."
78	)]
79	OnlyProvider,
80	/// Account cannot exist with the funds that would be given.
81	#[error("Account cannot exist with the funds that would be given.")]
82	BelowMinimum,
83	/// Account cannot be created.
84	#[error("Account cannot be created.")]
85	CannotCreate,
86	/// The asset in question is unknown.
87	#[error("The asset in question is unknown.")]
88	UnknownAsset,
89	/// Funds exist but are frozen.
90	#[error("Funds exist but are frozen.")]
91	Frozen,
92	/// Operation is not supported by the asset.
93	#[error("Operation is not supported by the asset.")]
94	Unsupported,
95	/// Account cannot be created for a held balance.
96	#[error("Account cannot be created for a held balance.")]
97	CannotCreateHold,
98	/// Withdrawal would cause unwanted loss of account.
99	#[error("Withdrawal would cause unwanted loss of account.")]
100	NotExpendable,
101	/// Account cannot receive the assets.
102	#[error("Account cannot receive the assets.")]
103	Blocked,
104}
105
106/// An error relating to arithmetic when dispatching a transaction.
107#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum ArithmeticError {
110	/// Underflow.
111	#[error("Underflow.")]
112	Underflow,
113	/// Overflow.
114	#[error("Overflow.")]
115	Overflow,
116	/// Division by zero.
117	#[error("Division by zero.")]
118	DivisionByZero,
119}
120
121/// An error relating to the transactional layers when dispatching a transaction.
122#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
123#[non_exhaustive]
124pub enum TransactionalError {
125	/// Too many transactional layers have been spawned.
126	#[error("Too many transactional layers have been spawned.")]
127	LimitReached,
128	/// A transactional layer was expected, but does not exist.
129	#[error("A transactional layer was expected, but does not exist.")]
130	NoLayer,
131}
132
133/// Details about a module error that has occurred.
134#[derive(Clone, thiserror::Error)]
135#[non_exhaustive]
136pub struct ModuleError {
137	metadata: Metadata,
138	/// Bytes representation:
139	///  - `bytes[0]`:   pallet index
140	///  - `bytes[1]`:   error index
141	///  - `bytes[2..]`: 3 bytes specific for the module error
142	bytes: [u8; 5],
143}
144
145impl PartialEq for ModuleError {
146	fn eq(&self, other: &Self) -> bool {
147		// A module error is the same if the raw underlying details are the same.
148		self.bytes == other.bytes
149	}
150}
151
152impl Eq for ModuleError {}
153
154/// Custom `Debug` implementation, ignores the very large `metadata` field, using it instead (as
155/// intended) to resolve the actual pallet and error names. This is much more useful for debugging.
156impl Debug for ModuleError {
157	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158		let details = self.details_string();
159		write!(f, "ModuleError(<{details}>)")
160	}
161}
162
163impl std::fmt::Display for ModuleError {
164	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165		let details = self.details_string();
166		write!(f, "{details}")
167	}
168}
169
170impl ModuleError {
171	/// Return more details about this error.
172	pub fn details(&self) -> Result<ModuleErrorDetails<'_>, ModuleErrorDetailsError> {
173		let pallet = self
174			.metadata
175			.pallet_by_error_index(self.pallet_index())
176			.ok_or(ModuleErrorDetailsError::PalletNotFound { pallet_index: self.pallet_index() })?;
177
178		let variant = pallet.error_variant_by_index(self.error_index()).ok_or_else(|| {
179			ModuleErrorDetailsError::ErrorVariantNotFound {
180				pallet_name: pallet.name().into(),
181				error_index: self.error_index(),
182			}
183		})?;
184
185		Ok(ModuleErrorDetails { pallet, variant })
186	}
187
188	/// Return a formatted string of the resolved error details for debugging/display purposes.
189	pub fn details_string(&self) -> String {
190		match self.details() {
191			Ok(details) => format!(
192				"{pallet_name}::{variant_name}",
193				pallet_name = details.pallet.name(),
194				variant_name = details.variant.name,
195			),
196			Err(_) => format!(
197				"Unknown pallet error '{bytes:?}' (pallet and error details cannot be retrieved)",
198				bytes = self.bytes
199			),
200		}
201	}
202
203	/// Return the underlying module error data that was decoded.
204	pub fn bytes(&self) -> [u8; 5] {
205		self.bytes
206	}
207
208	/// Obtain the pallet index from the underlying byte data.
209	pub fn pallet_index(&self) -> u8 {
210		self.bytes[0]
211	}
212
213	/// Obtain the error index from the underlying byte data.
214	pub fn error_index(&self) -> u8 {
215		self.bytes[1]
216	}
217
218	/// Attempts to decode the ModuleError into the top outer Error enum.
219	pub fn as_root_error<E: DecodeAsType>(&self) -> Result<E, ModuleErrorDecodeError> {
220		let decoded = E::decode_as_type(
221			&mut &self.bytes[..],
222			self.metadata.outer_enums().error_enum_ty(),
223			self.metadata.types(),
224		)
225		.map_err(ModuleErrorDecodeError)?;
226
227		Ok(decoded)
228	}
229}
230
231/// Details about the module error.
232pub struct ModuleErrorDetails<'a> {
233	/// The pallet that the error is in
234	pub pallet: pezkuwi_subxt_metadata::PalletMetadata<'a>,
235	/// The variant representing the error
236	pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
237}
238
239impl DispatchError {
240	/// Attempt to decode a runtime [`DispatchError`].
241	#[doc(hidden)]
242	pub fn decode_from<'a>(
243		bytes: impl Into<Cow<'a, [u8]>>,
244		metadata: Metadata,
245	) -> Result<Self, DispatchErrorDecodeError> {
246		let bytes = bytes.into();
247		let dispatch_error_ty_id = metadata
248			.dispatch_error_ty()
249			.ok_or(DispatchErrorDecodeError::DispatchErrorTypeIdNotFound)?;
250
251		// The aim is to decode our bytes into roughly this shape. This is copied from
252		// `sp_runtime::DispatchError`; we need the variant names and any inner variant
253		// names/shapes to line up in order for decoding to be successful.
254		#[derive(scale_decode::DecodeAsType)]
255		enum DecodedDispatchError {
256			Other,
257			CannotLookup,
258			BadOrigin,
259			Module(DecodedModuleErrorBytes),
260			ConsumerRemaining,
261			NoProviders,
262			TooManyConsumers,
263			Token(TokenError),
264			Arithmetic(ArithmeticError),
265			Transactional(TransactionalError),
266			Exhausted,
267			Corruption,
268			Unavailable,
269			RootNotAllowed,
270		}
271
272		// ModuleError is a bit special; we want to support being decoded from either
273		// a legacy format of 2 bytes, or a newer format of 5 bytes. So, just grab the bytes
274		// out when decoding to manually work with them.
275		struct DecodedModuleErrorBytes(Vec<u8>);
276		struct DecodedModuleErrorBytesVisitor<R: TypeResolver>(PhantomData<R>);
277		impl<R: TypeResolver> scale_decode::Visitor for DecodedModuleErrorBytesVisitor<R> {
278			type Error = scale_decode::Error;
279			type Value<'scale, 'info> = DecodedModuleErrorBytes;
280			type TypeResolver = R;
281
282			fn unchecked_decode_as_type<'scale, 'info>(
283				self,
284				input: &mut &'scale [u8],
285				_type_id: R::TypeId,
286				_types: &'info R,
287			) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>> {
288				DecodeAsTypeResult::Decoded(Ok(DecodedModuleErrorBytes(input.to_vec())))
289			}
290		}
291
292		impl scale_decode::IntoVisitor for DecodedModuleErrorBytes {
293			type AnyVisitor<R: TypeResolver> = DecodedModuleErrorBytesVisitor<R>;
294			fn into_visitor<R: TypeResolver>() -> DecodedModuleErrorBytesVisitor<R> {
295				DecodedModuleErrorBytesVisitor(PhantomData)
296			}
297		}
298
299		// Decode into our temporary error:
300		let decoded_dispatch_err = DecodedDispatchError::decode_as_type(
301			&mut &*bytes,
302			dispatch_error_ty_id,
303			metadata.types(),
304		)
305		.map_err(DispatchErrorDecodeError::CouldNotDecodeDispatchError)?;
306
307		// Convert into the outward-facing error, mainly by handling the Module variant.
308		let dispatch_error = match decoded_dispatch_err {
309			// Mostly we don't change anything from our decoded to our outward-facing error:
310			DecodedDispatchError::Other => DispatchError::Other,
311			DecodedDispatchError::CannotLookup => DispatchError::CannotLookup,
312			DecodedDispatchError::BadOrigin => DispatchError::BadOrigin,
313			DecodedDispatchError::ConsumerRemaining => DispatchError::ConsumerRemaining,
314			DecodedDispatchError::NoProviders => DispatchError::NoProviders,
315			DecodedDispatchError::TooManyConsumers => DispatchError::TooManyConsumers,
316			DecodedDispatchError::Token(val) => DispatchError::Token(val),
317			DecodedDispatchError::Arithmetic(val) => DispatchError::Arithmetic(val),
318			DecodedDispatchError::Transactional(val) => DispatchError::Transactional(val),
319			DecodedDispatchError::Exhausted => DispatchError::Exhausted,
320			DecodedDispatchError::Corruption => DispatchError::Corruption,
321			DecodedDispatchError::Unavailable => DispatchError::Unavailable,
322			DecodedDispatchError::RootNotAllowed => DispatchError::RootNotAllowed,
323			// But we apply custom logic to transform the module error into the outward facing
324			// version:
325			DecodedDispatchError::Module(module_bytes) => {
326				let module_bytes = module_bytes.0;
327
328				// The old version is 2 bytes; a pallet and error index.
329				// The new version is 5 bytes; a pallet and error index and then 3 extra bytes.
330				let bytes = if module_bytes.len() == 2 {
331					[module_bytes[0], module_bytes[1], 0, 0, 0]
332				} else if module_bytes.len() == 5 {
333					[
334						module_bytes[0],
335						module_bytes[1],
336						module_bytes[2],
337						module_bytes[3],
338						module_bytes[4],
339					]
340				} else {
341					tracing::warn!(
342						"Can't decode error sp_runtime::DispatchError: bytes do not match known shapes"
343					);
344					// Return _all_ of the bytes; every "unknown" return should be consistent.
345					return Err(DispatchErrorDecodeError::CouldNotDecodeModuleError {
346						bytes: bytes.to_vec(),
347					});
348				};
349
350				// And return our outward-facing version:
351				DispatchError::Module(ModuleError { metadata, bytes })
352			},
353		};
354
355		Ok(dispatch_error)
356	}
357}