diff --git a/crates/float/src/error.rs b/crates/float/src/error.rs index c290a2c..9e86ee2 100644 --- a/crates/float/src/error.rs +++ b/crates/float/src/error.rs @@ -51,6 +51,40 @@ pub enum DecimalFloatErrorSelector { WithTargetExponentOverflow, } +impl DecimalFloatErrorSelector { + /// A detailed, human-readable description of the underlying Solidity + /// `DecimalFloat` error that produced this selector. + pub fn to_readable_msg(&self) -> &'static str { + match self { + Self::CoefficientOverflow => { + "The number's coefficient is too large to fit in a Float (the signed coefficient exceeds the 224-bit range)." + } + Self::ExponentOverflow => { + "The number is too large to represent as a Float (its exponent exceeds the maximum supported magnitude)." + } + Self::ExponentUnderflow => { + "The number is too small to represent as a Float (its exponent is below the minimum supported magnitude, so it cannot be distinguished from zero)." + } + Self::FixedDecimalOverflow => { + "The number is too large to convert to a fixed-decimal value at the requested number of decimals (the scaled value exceeds the unsigned 256-bit range)." + } + Self::Log10Negative => { + "Cannot take the base-10 logarithm of a negative number." + } + Self::Log10Zero => "Cannot take the base-10 logarithm of zero.", + Self::LossyConversionFromFloat => { + "Converting this Float to the requested type would lose precision, and a lossless conversion was required." + } + Self::NegativeFixedDecimalConversion => { + "Cannot convert a negative number to an unsigned fixed-decimal value." + } + Self::WithTargetExponentOverflow => { + "The number cannot be rescaled to the requested target exponent without overflowing the Float coefficient." + } + } + } +} + impl TryFrom> for DecimalFloatErrorSelector { type Error = FixedBytes<4>; @@ -81,11 +115,64 @@ impl TryFrom> for DecimalFloatErrorSelector { } } +impl FloatError { + /// A detailed, human-readable description of the error, suitable for + /// surfacing to end users (e.g. via [WasmEncodedError::readable_msg]). + pub fn to_readable_msg(&self) -> String { + match self { + Self::Evm(e) => { + format!("An error occurred while executing the Float operation in the EVM: {e}") + } + Self::Revert(bytes) => { + format!("The Float operation reverted with output: {bytes}") + } + Self::Halt(reason) => { + format!("The Float operation halted unexpectedly with reason: {reason:?}") + } + Self::UnexpectedSuccess(reason, output) => { + format!( + "The Float operation ended for an unexpected non-return reason: {reason:?}. Output: {output:?}" + ) + } + Self::AlloySolTypes(e) => { + format!("Failed to encode or decode the Float operation's ABI data: {e}") + } + Self::DecimalFloat(e) => { + format!("The Float operation failed with a decimal float error: {e:?}") + } + Self::DecimalFloatSelector(selector) => match selector { + Ok(selector) => selector.to_readable_msg().to_string(), + Err(unknown) => format!( + "The Float operation reverted with an unrecognised error selector: {unknown}" + ), + }, + Self::Access(e) => { + format!("Failed to access the thread-local EVM used to run Float operations: {e}") + } + Self::InvalidHex(s) => { + format!("The provided value is not a valid hex string: {s}") + } + Self::AlloyFromHexError(e) => { + format!("Failed to decode the provided hex string: {e}") + } + Self::AlloyParseError(e) => { + format!("Failed to parse the provided number: {e}") + } + Self::AlloyParseSignedError(e) => { + format!("Failed to parse the provided signed number: {e}") + } + Self::JsSysError(s) => { + format!("A JavaScript error occurred while running the Float operation: {s}") + } + } + } +} + impl From for WasmEncodedError { fn from(value: FloatError) -> Self { WasmEncodedError { msg: value.to_string(), - readable_msg: value.to_string(), // todo: add detailed readable msg for errors + readable_msg: value.to_readable_msg(), } } } @@ -101,3 +188,97 @@ impl From for FloatError { FloatError::JsSysError(value.to_string().into()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decimal_float_error_selector_readable_msgs() { + assert_eq!( + DecimalFloatErrorSelector::CoefficientOverflow.to_readable_msg(), + "The number's coefficient is too large to fit in a Float (the signed coefficient exceeds the 224-bit range)." + ); + assert_eq!( + DecimalFloatErrorSelector::ExponentOverflow.to_readable_msg(), + "The number is too large to represent as a Float (its exponent exceeds the maximum supported magnitude)." + ); + assert_eq!( + DecimalFloatErrorSelector::ExponentUnderflow.to_readable_msg(), + "The number is too small to represent as a Float (its exponent is below the minimum supported magnitude, so it cannot be distinguished from zero)." + ); + assert_eq!( + DecimalFloatErrorSelector::FixedDecimalOverflow.to_readable_msg(), + "The number is too large to convert to a fixed-decimal value at the requested number of decimals (the scaled value exceeds the unsigned 256-bit range)." + ); + assert_eq!( + DecimalFloatErrorSelector::Log10Negative.to_readable_msg(), + "Cannot take the base-10 logarithm of a negative number." + ); + assert_eq!( + DecimalFloatErrorSelector::Log10Zero.to_readable_msg(), + "Cannot take the base-10 logarithm of zero." + ); + assert_eq!( + DecimalFloatErrorSelector::LossyConversionFromFloat.to_readable_msg(), + "Converting this Float to the requested type would lose precision, and a lossless conversion was required." + ); + assert_eq!( + DecimalFloatErrorSelector::NegativeFixedDecimalConversion.to_readable_msg(), + "Cannot convert a negative number to an unsigned fixed-decimal value." + ); + assert_eq!( + DecimalFloatErrorSelector::WithTargetExponentOverflow.to_readable_msg(), + "The number cannot be rescaled to the requested target exponent without overflowing the Float coefficient." + ); + } + + #[test] + fn test_float_error_readable_msg_invalid_hex() { + let err = FloatError::InvalidHex("zz".to_string()); + assert_eq!( + err.to_readable_msg(), + "The provided value is not a valid hex string: zz" + ); + } + + #[test] + fn test_float_error_readable_msg_js_sys() { + let err = FloatError::JsSysError("boom".to_string()); + assert_eq!( + err.to_readable_msg(), + "A JavaScript error occurred while running the Float operation: boom" + ); + } + + #[test] + fn test_float_error_readable_msg_decimal_float_selector_known() { + let err = FloatError::DecimalFloatSelector(Ok(DecimalFloatErrorSelector::Log10Zero)); + assert_eq!( + err.to_readable_msg(), + "Cannot take the base-10 logarithm of zero." + ); + } + + #[test] + fn test_float_error_readable_msg_decimal_float_selector_unknown() { + let unknown = FixedBytes::<4>::from([0xde, 0xad, 0xbe, 0xef]); + let err = FloatError::DecimalFloatSelector(Err(unknown)); + assert_eq!( + err.to_readable_msg(), + "The Float operation reverted with an unrecognised error selector: 0xdeadbeef" + ); + } + + #[test] + fn test_wasm_encoded_error_uses_readable_msg() { + let err = FloatError::InvalidHex("zz".to_string()); + let short = err.to_string(); + let readable = err.to_readable_msg(); + let encoded: WasmEncodedError = err.into(); + assert_eq!(encoded.msg, short); + assert_eq!(encoded.readable_msg, readable); + // The detailed readable message is distinct from the short message. + assert_ne!(encoded.msg, encoded.readable_msg); + } +}