Skip to main content

rain_math_float/
error.rs

1use crate::DecimalFloat;
2use alloy::hex::FromHexError;
3use alloy::primitives::{Bytes, FixedBytes};
4use alloy::sol_types::SolError;
5use revm::context::result::{EVMError, HaltReason, Output, SuccessReason};
6use std::thread::AccessError;
7use thiserror::Error;
8use wasm_bindgen_utils::prelude::js_sys::{Error as JsError, RangeError};
9use wasm_bindgen_utils::result::WasmEncodedError;
10
11#[derive(Debug, Error)]
12pub enum FloatError {
13    #[error("EVM error: {0}")]
14    Evm(#[from] EVMError<std::convert::Infallible>),
15    #[error("Float execution reverted with output: {0}")]
16    Revert(Bytes),
17    #[error("Float execution halted with reason: {0:?}")]
18    Halt(HaltReason),
19    #[error("Execution ended for non-return reason. Reason: {0:?}. Output: {1:?}")]
20    UnexpectedSuccess(SuccessReason, Output),
21    #[error(transparent)]
22    AlloySolTypes(#[from] alloy::sol_types::Error),
23    #[error("Decimal Float error: {0:?}")]
24    DecimalFloat(Box<DecimalFloat::DecimalFloatErrors>),
25    #[error("Decimal Float error selector: {0:?}")]
26    DecimalFloatSelector(Result<DecimalFloatErrorSelector, FixedBytes<4>>),
27    #[error(transparent)]
28    Access(#[from] AccessError),
29    #[error("Invalid hex string: {0}")]
30    InvalidHex(String),
31    #[error(transparent)]
32    AlloyFromHexError(#[from] FromHexError),
33    #[error(transparent)]
34    AlloyParseError(#[from] alloy::primitives::ruint::ParseError),
35    #[error(transparent)]
36    AlloyParseSignedError(#[from] alloy::primitives::ParseSignedError),
37    #[error("Wasm bindgen js_sys threw error: {0}")]
38    JsSysError(String),
39}
40
41#[derive(Debug)]
42pub enum DecimalFloatErrorSelector {
43    CoefficientOverflow,
44    ExponentOverflow,
45    ExponentUnderflow,
46    FixedDecimalOverflow,
47    Log10Negative,
48    Log10Zero,
49    LossyConversionFromFloat,
50    NegativeFixedDecimalConversion,
51    WithTargetExponentOverflow,
52}
53
54impl DecimalFloatErrorSelector {
55    /// A detailed, human-readable description of the underlying Solidity
56    /// `DecimalFloat` error that produced this selector.
57    pub fn to_readable_msg(&self) -> &'static str {
58        match self {
59            Self::CoefficientOverflow => {
60                "The number's coefficient is too large to fit in a Float (the signed coefficient exceeds the 224-bit range)."
61            }
62            Self::ExponentOverflow => {
63                "The number is too large to represent as a Float (its exponent exceeds the maximum supported magnitude)."
64            }
65            Self::ExponentUnderflow => {
66                "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)."
67            }
68            Self::FixedDecimalOverflow => {
69                "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)."
70            }
71            Self::Log10Negative => {
72                "Cannot take the base-10 logarithm of a negative number."
73            }
74            Self::Log10Zero => "Cannot take the base-10 logarithm of zero.",
75            Self::LossyConversionFromFloat => {
76                "Converting this Float to the requested type would lose precision, and a lossless conversion was required."
77            }
78            Self::NegativeFixedDecimalConversion => {
79                "Cannot convert a negative number to an unsigned fixed-decimal value."
80            }
81            Self::WithTargetExponentOverflow => {
82                "The number cannot be rescaled to the requested target exponent without overflowing the Float coefficient."
83            }
84        }
85    }
86}
87
88impl TryFrom<FixedBytes<4>> for DecimalFloatErrorSelector {
89    type Error = FixedBytes<4>;
90
91    fn try_from(error_selector: FixedBytes<4>) -> Result<Self, Self::Error> {
92        let FixedBytes(bytes) = error_selector;
93        match bytes {
94            <DecimalFloat::CoefficientOverflow as SolError>::SELECTOR => {
95                Ok(Self::CoefficientOverflow)
96            }
97            <DecimalFloat::ExponentOverflow as SolError>::SELECTOR => Ok(Self::ExponentOverflow),
98            <DecimalFloat::ExponentUnderflow as SolError>::SELECTOR => Ok(Self::ExponentUnderflow),
99            <DecimalFloat::FixedDecimalOverflow as SolError>::SELECTOR => {
100                Ok(Self::FixedDecimalOverflow)
101            }
102            <DecimalFloat::Log10Negative as SolError>::SELECTOR => Ok(Self::Log10Negative),
103            <DecimalFloat::Log10Zero as SolError>::SELECTOR => Ok(Self::Log10Zero),
104            <DecimalFloat::LossyConversionFromFloat as SolError>::SELECTOR => {
105                Ok(Self::LossyConversionFromFloat)
106            }
107            <DecimalFloat::NegativeFixedDecimalConversion as SolError>::SELECTOR => {
108                Ok(Self::NegativeFixedDecimalConversion)
109            }
110            <DecimalFloat::WithTargetExponentOverflow as SolError>::SELECTOR => {
111                Ok(Self::WithTargetExponentOverflow)
112            }
113            _ => Err(error_selector),
114        }
115    }
116}
117
118impl FloatError {
119    /// A detailed, human-readable description of the error, suitable for
120    /// surfacing to end users (e.g. via [WasmEncodedError::readable_msg]).
121    pub fn to_readable_msg(&self) -> String {
122        match self {
123            Self::Evm(e) => {
124                format!("An error occurred while executing the Float operation in the EVM: {e}")
125            }
126            Self::Revert(bytes) => {
127                format!("The Float operation reverted with output: {bytes}")
128            }
129            Self::Halt(reason) => {
130                format!("The Float operation halted unexpectedly with reason: {reason:?}")
131            }
132            Self::UnexpectedSuccess(reason, output) => {
133                format!(
134                    "The Float operation ended for an unexpected non-return reason: {reason:?}. Output: {output:?}"
135                )
136            }
137            Self::AlloySolTypes(e) => {
138                format!("Failed to encode or decode the Float operation's ABI data: {e}")
139            }
140            Self::DecimalFloat(e) => {
141                format!("The Float operation failed with a decimal float error: {e:?}")
142            }
143            Self::DecimalFloatSelector(selector) => match selector {
144                Ok(selector) => selector.to_readable_msg().to_string(),
145                Err(unknown) => format!(
146                    "The Float operation reverted with an unrecognised error selector: {unknown}"
147                ),
148            },
149            Self::Access(e) => {
150                format!("Failed to access the thread-local EVM used to run Float operations: {e}")
151            }
152            Self::InvalidHex(s) => {
153                format!("The provided value is not a valid hex string: {s}")
154            }
155            Self::AlloyFromHexError(e) => {
156                format!("Failed to decode the provided hex string: {e}")
157            }
158            Self::AlloyParseError(e) => {
159                format!("Failed to parse the provided number: {e}")
160            }
161            Self::AlloyParseSignedError(e) => {
162                format!("Failed to parse the provided signed number: {e}")
163            }
164            Self::JsSysError(s) => {
165                format!("A JavaScript error occurred while running the Float operation: {s}")
166            }
167        }
168    }
169}
170
171impl From<FloatError> for WasmEncodedError {
172    fn from(value: FloatError) -> Self {
173        WasmEncodedError {
174            msg: value.to_string(),
175            readable_msg: value.to_readable_msg(),
176        }
177    }
178}
179
180impl From<JsError> for FloatError {
181    fn from(value: JsError) -> Self {
182        FloatError::JsSysError(value.to_string().into())
183    }
184}
185
186impl From<RangeError> for FloatError {
187    fn from(value: RangeError) -> Self {
188        FloatError::JsSysError(value.to_string().into())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_decimal_float_error_selector_readable_msgs() {
198        assert_eq!(
199            DecimalFloatErrorSelector::CoefficientOverflow.to_readable_msg(),
200            "The number's coefficient is too large to fit in a Float (the signed coefficient exceeds the 224-bit range)."
201        );
202        assert_eq!(
203            DecimalFloatErrorSelector::ExponentOverflow.to_readable_msg(),
204            "The number is too large to represent as a Float (its exponent exceeds the maximum supported magnitude)."
205        );
206        assert_eq!(
207            DecimalFloatErrorSelector::ExponentUnderflow.to_readable_msg(),
208            "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)."
209        );
210        assert_eq!(
211            DecimalFloatErrorSelector::FixedDecimalOverflow.to_readable_msg(),
212            "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)."
213        );
214        assert_eq!(
215            DecimalFloatErrorSelector::Log10Negative.to_readable_msg(),
216            "Cannot take the base-10 logarithm of a negative number."
217        );
218        assert_eq!(
219            DecimalFloatErrorSelector::Log10Zero.to_readable_msg(),
220            "Cannot take the base-10 logarithm of zero."
221        );
222        assert_eq!(
223            DecimalFloatErrorSelector::LossyConversionFromFloat.to_readable_msg(),
224            "Converting this Float to the requested type would lose precision, and a lossless conversion was required."
225        );
226        assert_eq!(
227            DecimalFloatErrorSelector::NegativeFixedDecimalConversion.to_readable_msg(),
228            "Cannot convert a negative number to an unsigned fixed-decimal value."
229        );
230        assert_eq!(
231            DecimalFloatErrorSelector::WithTargetExponentOverflow.to_readable_msg(),
232            "The number cannot be rescaled to the requested target exponent without overflowing the Float coefficient."
233        );
234    }
235
236    #[test]
237    fn test_float_error_readable_msg_invalid_hex() {
238        let err = FloatError::InvalidHex("zz".to_string());
239        assert_eq!(
240            err.to_readable_msg(),
241            "The provided value is not a valid hex string: zz"
242        );
243    }
244
245    #[test]
246    fn test_float_error_readable_msg_js_sys() {
247        let err = FloatError::JsSysError("boom".to_string());
248        assert_eq!(
249            err.to_readable_msg(),
250            "A JavaScript error occurred while running the Float operation: boom"
251        );
252    }
253
254    #[test]
255    fn test_float_error_readable_msg_decimal_float_selector_known() {
256        let err = FloatError::DecimalFloatSelector(Ok(DecimalFloatErrorSelector::Log10Zero));
257        assert_eq!(
258            err.to_readable_msg(),
259            "Cannot take the base-10 logarithm of zero."
260        );
261    }
262
263    #[test]
264    fn test_float_error_readable_msg_decimal_float_selector_unknown() {
265        let unknown = FixedBytes::<4>::from([0xde, 0xad, 0xbe, 0xef]);
266        let err = FloatError::DecimalFloatSelector(Err(unknown));
267        assert_eq!(
268            err.to_readable_msg(),
269            "The Float operation reverted with an unrecognised error selector: 0xdeadbeef"
270        );
271    }
272
273    #[test]
274    fn test_wasm_encoded_error_uses_readable_msg() {
275        let err = FloatError::InvalidHex("zz".to_string());
276        let short = err.to_string();
277        let readable = err.to_readable_msg();
278        let encoded: WasmEncodedError = err.into();
279        assert_eq!(encoded.msg, short);
280        assert_eq!(encoded.readable_msg, readable);
281        // The detailed readable message is distinct from the short message.
282        assert_ne!(encoded.msg, encoded.readable_msg);
283    }
284}